C++11 – 函数模版可变参数
可变模版参数是C++11新增最强大的特性之一,它对参数的高度泛化,能够让我们创建可以接受可变参数的函数模版和类模版
- C++11之前,类模版和函数模版中只能够包含固定数量的模版参数,可变模版参数无疑是一个巨大的改进,但由于可变模版参数比较抽象,使用需要一定的技巧
- C++11之前其实也有可变参数的概念,如printf就能够接受多个参数,但是这是函数参数的可变参数,并不是模版
可变参数的定义方式
template<class ...Args>
void ShowArgs(Args... args){}
- 模版参数
...Args
三个点代表它是一个可变参数,我们将带省略号的参数称为参数包,参数包里面可以包含0到N个模版参数,而args则是一个函数形参参数包 - 模版参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫Args或args
sizeof…获取参数包参数个数
我们可以使用sizeof...()
来 计算参数包到底包含多少个参数,这些参数可以是不同类型的
template<class ...Args>
void ShowArgs(Args... args){
std::cout << sizeof...(args) << std::endl; // 获取参数包参数个数
}
void test1() {
ShowArgs();
ShowArgs(1);
ShowArgs(1, '2', "hello world");
}
注意⚠️:语法并不支持使用arg[i]的方式获取参数包中的参数,其中的参数智能通过展开参数包的方式来获取,这是使用可变参数模版的一个主要特点也是最大的难点
参数包展开方式
递归展开参数包
- 给函数模版一个模版参数,这样就可以从接受到的参数包中分离一个参数出来。
- 在函数模版中递归调用该函数模版,调用时传入剩下的参数包
- 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的参数被全部取出
如果我们想要打印参数包中的参数可以这样写
void ShowListArgs(){ // 用于终止递归
std::cout << "get args end" << std::endl;
}
template<class T, class ...Args>
void ShowListArgs(T value, Args... args) {
std::cout << value << std::endl; // 打印分离参数
ShowListArgs(args...); // 递归调用,将参数包向下传递
}
template<class ...Args>
void ShowList(Args... args) { // 供外部调用
ShowListArgs(args...);
}
void test2() {
ShowList("hello", "variable", "template", "parameter");
}
最上面的函数是递归终止函数,当没有参数包内没有参数时就不会通过模版生成新的函数而是直接调用我们的递归终止函数,这样就结束了递归。但是如果外界本来就没有传入参数,那么就会直接匹配到无参递归函数,导致调用函数会随着外部传入参数变化而变化
鉴于此,我们将展开函数和终止函数名进行更改将其设为自函数,被ShowList函数进行调用,这样不管外部如何传参,调用到的都是同一个函数了
使用参数包中的参数个数终止递归(不可行)
// 错误实例
template<class T, class ...Args>
void ShowList(T value, Args... args) {
if (size...(args) == 0) {
return;
}
std::cout << value << std::endl;
ShowList(args...);
}
这种方式是不可行的,因为函数模版不可以被调用,函数模版需要在编译时根据传入的实参进行推演,生成对应的函数,这个生成的函数才可以被调用
而这个推演过程在编译时会进行,当推延到args参数个数为0时,还需要将当前函数推演完成,这时候就会推演传入0个参数到ShowList函数中,此时就会出错,因为ShowList函数至少需要一个参数。这里的if是代码编译生成完成后,代码运行才会走的逻辑,而函数模版的推演是编译时的逻辑
逗号表达式展开参数包
通过列表获取参数包中的参数
数组可以用于列表出实话,如果参数包中的数据类型都是整形,那么就可以将这个参数包放到列表当中初始化这个整形数组,比如
template<class ...Args>
void ShowList2(Args... args) {
int arr[] = {args...} ;
for (auto e : arr) {
std::cout << e << std::endl;
}
std::cout << std::endl;
}
void test3() {
ShowList2(1);
ShowList2(1, 2, 3);
}
但是C++并不想Python那样,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList2函数传入的参数只能是整形,并且C++不支持元素个数为0的数组,所以也不能传入零个参数,因此数组的大小也不能为0
逗号表达式展开参数包
- 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回
- 逗号表达式的最后一个表达式设置成一个整形值,确保逗号表达式至少有一个元素并且是整形
- 将处理参数包中的参数的动作封装成一个函数,将其作为逗号表达式中的第一个值
这样以来,在执行逗号表达式时就会调用处理函数依次处理参数包中的参数,最后将逗号表达式中的最后一个整形值作为返回值初始化整形数组 比如
template<class T>
void PrintArg(const T& value) {
std::cout << value << std::endl;
}
template<class ...Args>
void ShowList3(Args... args) {
int arr[] = { (PrintArg(args), 0)... }; // 列表初始化,逗号表达式
std::cout << std::endl;
}
void test4() {
ShowList3(1, 3, "hello");
}
注意⚠️:可变参数的省略号需要加在逗号表达式的外面,表示要将逗号表达式展开,如果省略号加载args后面就会将所有的参数都传给PrintArg函数
这时ShowList3函数就可以传入多个不同类型的参数了,但是调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入0个参数,可以写一个无参的函数
实际上我们也可以不使用逗号表达式,比如我们可以将处理函数的返回值设置成整形,通过这个返回值初始化数组也是可以的
STL中的emplace接口函数
C++11标准给STL容器增加了emplace版本的插入接口,比如list容器的push_front,push_back,insert函数都增加了emplace_front, emplace_back, emplace 函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSefUcEh-1689471486874)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230715142523454.png)]
这些emplace版本的插入接口支持模版的可变参数,这些借口的使用和容器原油的插入接口使用方法类似,略微有一些不同,我们用list容器来举例
- 调用push_back插入元素时,可以传入左值对象或者右值对象,但是并不可以使用列表初始化
- 调用emplace_back插入元素,既可以传入左值也可以传入右值,但不能使用列表进行初始化
- 除此之外,emplace系列借口最大的特点就是,插入元素可以传入用于构造元素的参数包
void test5(){
std::list<std::pair<int, std::string>> mylist;
std::pair<int, std::string> kv(10, "111");
mylist.push_back(kv);
mylist.push_back(std::pair<int, std::string>(20, "222"));
mylist.push_back({30, "333"});
mylist.emplace_back(kv); // 构造 + 拷贝构造
mylist.emplace_back(std::pair<int, std::string>(20, "222")); // 构造 + 移动赋值
mylist.emplace_back(30, "333"); // 构造函数
}
emplace 接口实现步骤
1、首先会通过空间配置器获取一块内存空间(只开辟,并不初始化)
2、然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数会传入这块空间的地址和用户传入的参数(需经完美转发)
3、在allocator_traits::construct 函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化。调用构造函数时会传入用户的参数(经过完美转发)
4、将初始化好的新节点插入到对应的数据结构当中
emplace 接口的价值
-
最大的特点就是支持传入参数包,可以使用参数包直接构造出对象,这样就可以减少一次构造,这也是有人说emplace系列借口高效的原因。
-
但emplace在传入左值或者是右值对象时的效率和原有接口的效率是一样的