发布于2026-07-13 阅读(0)
扫一扫,手机访问
说明一下,这篇专题是复习用的,比较适合已经学过C++的同学,用来巩固可变参数模板这块内容。
可变参数模板是C++11引入的一个新特性,它让参数的高度泛化成为可能——我们终于可以创建那些能接受可变参数的函数模板和类模板了。
printf函数就能接收任意多个参数。但要注意,那是函数参数的可变参数,并不是模板的可变参数。注:这篇只讲函数模板的可变参数,类模板的不涉及。
函数的可变参数模板定义起来其实很直观,看下面这个格式:
template返回类型 函数名(Args...args) { // 函数体 }
举个例子:
templatevoid ShowList(Args... args) {}
注意这里的省略号:在模板参数列表中,class... 或 typename... 表示后面跟着的是一个类型包,可以匹配0个或多个类型;在函数参数列表中,类型名后面跟 ... 表示后面跟着的是一个形参对象包,同样可以匹配0个或多个对象。另外,函数参数包可以用左值引用或右值引用表示,每个参数实例化时都会遵循引用折叠规则。
这样一来,调用ShowList函数时就可以传入任意多个参数了,而且这些参数的类型可以各不相同。比如:
int main()
{
ShowList();
ShowList(1);
ShowList(1,'A');
ShowList(1,'A',string("hello"));
return 0;
}
我们还可以在函数模板中通过sizeof...来获取参数包中参数的个数:
templatevoid ShowList(Args... args) { cout << sizeof...(args) << endl; } int main() { ShowList(); // 0 ShowList(1); // 1 ShowList(1,'A'); // 2 ShowList(1,'A',string("hello")); // 3 return 0; }
但是有一个关键问题:我们没法直接通过下标来获取参数包中的每个参数,只能通过展开参数包的方式来拿到它们。这也是可变参数模板最大的特点和难点所在。
它的本质其实有两种理解方式:
1. 结合引用折叠规则,编译器会实例化出下面这些函数:
void ShowList(); void ShowList(int arg1); void ShowList(int arg1,char arg2); void ShowList(int arg1,char arg2,string arg3);
2. 更本质地看,生成的是下面的模板:
void ShowList(); templatevoid ShowList(T1 arg1); template void ShowList(T1 arg1,T2 arg2); template void ShowList(T1 arg1, T2 arg2,T3 arg3);
需要特别强调的是:语法并不支持用 args[i] 的方式来获取参数包中的参数。下面的写法是错误的:
templatevoid ShowList(Args... args) { for (int i = 0; i < sizeof...(args); i++) { cout << args[i] << " "; } cout << endl; }
所以,要获取参数包中的各个参数,只能通过展开参数包的方式来实现。常用的展开方式有两种:递归展开和逗号表达式展开。
递归展开的核心思路是这样的:
比如我们想打印调用函数时传入的各个参数,可以这样写:
//展开函数 templatevoid ShowList(T value, Args... args) { cout << value << " "; //打印分离出的第一个参数 ShowList(args...); //递归调用,将参数包继续向下传 }
接下来要解决的就是:递归怎么停下来?
编写无参的递归终止函数
我们可以再写一个无参的同名函数作为递归终止条件:
//递归终止函数
void ShowList()
{
cout << endl;
}
//展开函数
template
void ShowList(T value, Args... args)
{
cout << value << " "; //打印分离出的第一个参数
ShowList(args...); //递归调用,将参数包继续向下传
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', string("hello"));
return 0;
}
当参数包中的参数个数为0时,递归调用就会匹配到那个无参的终止函数,递归就结束了。
当然,也可以换一种写法,把ShowList和Print分开来:
void ShowList()
{
cout << endl;
}
template
void ShowList(T x, Args... args)
{
cout << x << " ";
ShowList(args...);
}
// 编译时递归推导解析参数
template
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print(1);
Print(1, 'A');
Print(1, 'A', string("hello"));
return 0;
}
1. 通过列表获取参数包中的参数
如果参数包中所有参数的类型都是整型,那就可以直接把这个参数包放到一个列表里,用来初始化一个整型数组。这样参数包中的参数就被放进数组里了:
templatevoid Print(Args...args) { int arr[] = { args...}; for (auto e : arr) { cout << e << " "; } cout << endl; } int main() { Print(1); Print(1,2); Print(1,2,3); return 0; }
但C++不像Python这类语言——C++要求容器里存储的数据类型必须一致。所以如果这么写,调用ShowList时传入的参数就只能是整型,而且还不能传0个参数,因为数组大小不能为0。要解决这个问题,就得借助逗号表达式。
2. 通过逗号表达式展开参数包
虽然不能用不同类型的参数去初始化整型数组,但逗号表达式可以帮上忙。
这样,在执行逗号表达式时,会先调用那个处理函数来处理对应的参数,然后用最后一个整型值去初始化整型数组。比如:
templateconst T& GetArg(const T& x) { cout << x << " "; return x; } template void Arguments(Args... args) {} template void Print(Args... args) { Arguments(GetArg(args)...); } int main() { Print(1, string("2345"), 1.1); return 0; }
它的本质其实是:编译器在编译时会实例化出下面这样的函数:
templateconst T& GetArg(const T& x) { cout << x << " "; return x; } template void Arguments(Args... args) {} template void Print(int x,string y,double z) { Arguments(GetArg(x),GetArg(y),GetArg(z)); } int main() { Print(1, string("2345"), 1.1); return 0; }
C++11标准给STL中的容器新增了emplace版本的插入接口。比如list容器的push_front、push_back和insert,都增加了对应的emplace_front、emplace_back和emplace函数。如下所示:


注意: emplace系列接口的可变模板参数类型都带有“&&”,这表示的是万能引用,不是右值引用。
emplace系列接口的使用方式
emplace系列接口的使用方式和容器原有的插入接口有些类似,但也有一些不一样的地方。
以list容器的emplace_back和push_back为例:
push_back插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。emplace_back插入元素时,同样可以传入左值对象或者右值对象,但不能使用列表进行初始化。emplace系列接口的意义
由于emplace系列接口的可变模板参数类型都是万能引用,所以既可以接收左值对象,也可以接收右值对象,还能接收参数包。
总结一下:
当然,这里有一个前提:容器中存储的元素对应的类,是一个需要深拷贝的类,并且该类实现了移动构造函数。否则,调用emplace系列接口时,传入左值对象和传入右值对象的效果是一样的——都需要一次构造函数加一次拷贝构造函数。
实际上,emplace系列接口的一部分功能和容器原有的插入接口是重叠的。因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口。如果调用这些接口时传入的是右值对象,最终也会调用对应的移动构造函数来移动资源。
emplace接口的意义:
验证
要验证上面的说法,需要一个深拷贝的类。下面模拟实现了一个简化版的string类,只写了我们需要的成员函数:
namespace xxhh
{
class string
{
public:
//构造函数
string(const char* str = "")
{
cout << "string(const char* str) -- 构造函数" << endl;
_size = strlen(str); //初始时,字符串大小设置为字符串长度
_capacity = _size; //初始时,字符串容量设置为字符串长度
_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
strcpy(_str, str); //将C字符串拷贝到已开好的空间
}
//交换两个对象的数据
void swap(string& s)
{
//调用库里的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
//拷贝构造函数(现代写法)
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
swap(tmp); //交换这两个对象
}
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
//拷贝赋值函数(现代写法)
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s); //用s拷贝构造出对象tmp
swap(tmp); //交换这两个对象
return *this; //返回左值(支持连续赋值)
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
//析构函数
~string()
{
//delete[] _str; //释放_str指向的空间
_str = nullptr; //及时置空,防止非法访问
_size = 0; //大小置0
_capacity = 0; //容量置0
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
因为我们在string的构造函数、拷贝构造函数和移动构造函数中都打印了提示语句,所以可以通过控制台输出来判断这些函数有没有被调用。
下面用一个容器来存储这个模拟的string,并以不同的传参形式调用emplace系列函数:
#includeint main() { list
> mylist; pair kv(1, "one"); mylist.emplace_back(kv); //传左值 cout << endl; mylist.emplace_back(pair (2, "two")); //传右值 cout << endl; mylist.emplace_back(3, "three"); //传参数包 return 0; }

售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8