
在 C++ 的世界里,模板机制一直是提升代码复用性和泛型编程能力的重要工具。而可变参数模板(Variadic Templates)作为 C++11 引入的一项强大特性,更是将模板的灵活性推向了新的高度。它允许我们编写能够处理任意数量、任意类型参数的函数和类,极大地增强了代码的通用性和适应性。
在传统 C++ 中,函数的参数数量和类型是固定的,这在很多情况下限制了函数的通用性。而可变参数模板的出现,打破了这一限制。它允许函数接受不确定数量的参数,这些参数可以是任意类型,从而让函数能够以更加灵活的方式处理各种不同的输入。
可变参数模板的核心是使用 ...(三个点)来表示参数包(parameter pack)。参数包可以看作是一个包含了多个参数的集合,这些参数在函数中可以被逐一处理。例如,以下是一个简单的可变参数模板函数的声明:
template<typename... Args>
void print(Args... args);在这个例子中,Args... args 就是一个参数,包Args 是一个模板参数列表,它代表了参数的类型,而 args 是参数包的名称。这个函数可以接受任意数量和类型的参数,这些参数在函数体内将被统一处理。
void ShowList()
{
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
ShowList(args...);
}
template <class ...Args>
void Print(Args... args)
{
// N个参数,第一个传给x,剩下N-1参数传给ShowList的第二个参数包
ShowList(args...);
}这段代码实现了一个递归打印的功能。Print 函数接收一个参数包 args,然后通过 ShowList(args...) 的方式将参数包展开,并将展开后的结果传递给 ShowList 函数。ShowList 函数通过递归的方式依次打印每个参数,直到参数包为空。
在我们的示例中,template <class ...Args> 定义了一个参数包 Args,它表示 Print 函数可以接受任意数量和类型的参数。
参数包展开是指将参数包中的每个参数依次展开,以便在代码中对每个参数进行操作。展开操作是通过 ... 运算符来完成的。
在 Print 函数中,ShowList(args...) 是参数包展开的关键。这里,args 是参数包,ShowList(args...) 的作用是将参数包中的每个参数依次传递给 ShowList 函数。
例如,如果调用 Print(1, 2.5, "hello"),那么参数包 args 包含三个参数:1、2.5 和 "hello"。ShowList(args...) 的展开过程如下:
ShowList(1, 2.5, "hello"),T 是 int,x 是 1,剩下的参数包 args 是 2.5 和 "hello"。ShowList(2.5, "hello"),T 是 double,x 是 2.5,剩下的参数包 args 是 "hello"。ShowList("hello"),T 是 const char*,x 是 "hello",剩下的参数包 args 是空的。ShowList(),参数包为空,打印换行符并结束递归。首先,让我们来看一下本文的核心代码示例:
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
// GetArg的返回值组成实参参数包,传给Arguments
Arguments(GetArg(args)...);
}这段代码中,Print 函数是关键。它接收一个参数包 args,然后通过 GetArg(args)... 的方式将参数包展开,并将展开后的结果作为参数传递给 Arguments 函数。
template <class ...Args> 定义了一个参数包 Args,它表示 Print 函数可以接受任意数量和类型的参数。
参数包展开是指将参数包中的每个参数依次展开,以便在代码中对每个参数进行操作。展开操作是通过 ... 运算符来完成的。
在 Print 函数中,GetArg(args)... 是参数包展开的关键。这里,args 是参数包,GetArg(args) 表示对参数包中的每个参数调用 GetArg 函数。... 运算符的作用是将 GetArg(args) 对每个参数的调用结果展开成一个参数列表,然后将这个参数列表传递给 Arguments 函数。
例如,如果调用 Print(1, 2.5, "hello"),那么参数包 args 包含三个参数:1、2.5 和 "hello"。GetArg(args)... 的展开过程如下:
1 调用 GetArg(1),得到返回值 1。2.5 调用 GetArg(2.5),得到返回值 2.5。"hello" 调用 GetArg("hello"),得到返回值 "hello"。最终,GetArg(args)... 展开成 GetArg(1), GetArg(2.5), GetArg("hello"),这相当于 1, 2.5, "hello",然后将这个参数列表传递给 Arguments 函数。
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position, Args&&... args);在 emplace 出现之前,我们往容器里添加元素时,常常会遇到一些效率问题。比如,当我们使用 std::vector 或 std::map 等容器时,如果要添加一个对象,通常的做法是先构造好对象,然后再将其拷贝或移动到容器中。这个过程涉及到临时对象的创建、拷贝或移动构造等操作,不仅代码显得繁琐,还可能带来不必要的性能开销,尤其是在处理大量数据或复杂对象时,这种开销会更加明显。
emplace 系列接口的登场C++11 引入了 emplace 系列接口,为容器操作带来了革命性的变化。emplace 的基本思想是在容器分配的存储空间上直接构造对象,从而避免了临时对象的创建和拷贝/移动构造等中间步骤。
以 std::vector 为例,其 emplace_back 方法允许我们在向量的末尾直接构造一个对象。假设我们有一个类 Person,包含姓名和年龄两个成员变量,我们想往 std::vector<Person> 中添加一个 Person 对象。传统的做法可能是这样:
std::vector<Person> people;
Person p("Alice", 25);
people.push_back(p);这里,Person 对象 p 先被构造出来,然后通过 push_back 方法拷贝到 std::vector 中,涉及到两次构造(一次是 p 的构造,一次是拷贝构造到向量中)。
而使用 emplace_back,我们可以这样写:
std::vector<Person> people;
people.emplace_back("Alice", 25);在这个例子中,emplace_back 直接在 std::vector 分配的存储空间上构造了一个 Person 对象,构造参数直接传递给 Person 的构造函数。这样,就避免了临时对象 p 的创建以及后续的拷贝构造,大大提高了效率。
emplace 系列接口的原理emplace 系列接口的魔法在于它利用了完美转发和变长参数模板。当调用 emplace 或 emplace_back 等方法时,这些方法会将传入的参数完美转发给容器中元素类型的构造函数,从而在容器分配的内存空间上直接构造对象。
以 std::map 的 emplace 方法为例,其大致实现原理如下:
template <typename Key, typename T, typename Compare, typename Allocator>
template <typename... Args>
std::pair<typename std::map<Key, T, Compare, Allocator>::iterator, bool>
std::map<Key, T, Compare, Allocator>::emplace(Args&&... args)
{
// 在合适的位置分配内存
auto position = ...;
// 完美转发参数给键值对的构造函数,在分配的内存上直接构造对象
auto result = emplace_hint(position, std::forward<Args>(args)...);
return result;
}这里的关键是 std::forward<Args>(args)...,它利用完美转发将参数原封不动地转发给元素类型的构造函数,从而保证了参数的值类别(左值或右值)不会改变,使得构造函数能够根据参数的实际类型进行正确的构造。
int main()
{
// 效率用法都是一样的
bit::list<int> lt1;
lt1.push_back(1);
lt1.emplace_back(2);
bit::list<bit::string> lt2;
// 传左值效率用法都是一样的
bit::string s1("111111111");
lt2.push_back(s1);
lt2.emplace_back(s1);
cout << "*****************************************" << endl;
// 传右值效率用法都是一样的
bit::string s2("111111111");
lt2.push_back(move(s2));
bit::string s3("111111111");
lt2.emplace_back(move(s3));
cout << "*****************************************" << endl;
// emplace_back的效率略高一筹
lt2.push_back("1111111111111111111111111111");
lt2.emplace_back("11111111111111111111111111");
cout << "*****************************************" << endl;
}成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化
修饰类:当一个类被final修饰时,意味着这个类不能被继承。
class Base final { /* ... */ };
class Derived : public Base { /* 错误:Base 是 final 类,不能被继承 */ };修饰虚函数:若虚函数被final修饰,那么在派生类中就不能再对该函数进行重写。
class Base {
public:
virtual void func() final; // 声明为 final
};
class Derived : public Base {
public:
void func(); // 错误:不能重写 final 函数
};在C++里,override用于显式表明派生类中的函数是对基类虚函数的重写。如果使用了override却没有成功重写基类虚函数,编译器就会报错。
class Base {
public:
virtual void func(int x);
};
class Derived : public Base {
public:
void func(int x) override; // 正确:重写基类虚函数
void func(double x) override; // 错误:基类中没有匹配的虚函数
};