可变参数模板的认识
c++11 引入了可变参数模板(variadic templates)的特性,使得编写支持任意数量参数的模板变得更加简单灵活。
c++11 的可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比 c++98/03 ,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进。然而由于可变参数模板比较抽象,使用起来需要一定的技巧,所以使用的不是很多。
下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args... args,这个参数包可以包含0到任意个模板参数
template<class ...Args>
void ShowList(Args... args)
{}
说明:
- 模板参数 Args 前面有省略号,表示它是一个可变模板参数,我们把带省略号的参数称为 “参数包”,它里面包含了 0 ~ N (N>=0) 个模板参数。args 则是一个函数形参参数包。
- 我们无法直接获取参数包 args 中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变参数模板参数的一个主要特点,也是最大的难点,即如何展开可变模板参数。
- 语法不支持使用 args[i] 这样的方式来获取可变参数。
- 模板参数包 Args 和函数形参参数包 args 的名字也是可以指定的,并不是固定的。
现在,可通过 ShowList 函数,传递任意数量和类型的参数,如下:
int main()
{
ShowList();
ShowList(5);
ShowList(3, 3.14, 'C', "template");
return 0;
}
我们可以通过 sizeof...
来获取函数模板参数包的个数:
template<class ...Args>
int getArgsCount(Args... args)
{
return sizeof...(args);
}
int main()
{
cout << getArgsCount(3, 3.14, 'C', "template") << endl; // 运行测试结果为:4
return 0;
}
参数包的展开
递归函数方式展开参数包
方式一:
递归展开参数包的方式如下:
- 给函数模板增加一个模板参数,这样就可以从参数包中分离出一个参数出来。
- 在函数模板中递归调用该函数模板,调用时传入剩余的参数包。
- 一直递归,每次取出参数包中的一个参数,直到参数中的所有参数被取出。
取出参数包中的每个参数并打印,函数模板的写法如下:
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " "; // 打印取出的参数
ShowList(args...); // 递归调用,将参数包传递下去
}
使用次函数的时候出现一个问题,即如何终止函数的递归调用。
无参的递归终止函数
// 无参递归终止函数
template<class T>
void ShowList() { cout << endl; }
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " "; // 打印取出的参数
ShowList(args...); // 递归调用,将参数包传递下去
}
当递归调用 ShowList 函数模板时,若传入的参数包中的参数个数为 0 时,就会匹配到这个无参的递归终止函数,这样就解决了终止递归的问题。
完整代码如下:
//递归终止函数
void ShowList()
{
cout << endl;
}
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " "; //打印传入的若干参数中的第一个参数
ShowList(args...); //将剩下参数继续向下传
}
int main()
{
ShowList();
ShowList(5);
ShowList(3, 3.14, 'C', "template");
return 0;
}
带参的递归终止函数
// 匹配无参的调用
template<class ...Args>
void ShowList(Args... args) {}
// 带参递归终止函数
template<class T>
void ShowList(const T& t)
{
cout << t << endl;
}
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " "; // 打印取出的参数
ShowList(args...); // 递归调用,将参数包传递下去
}
在调用 ShowList 函数时,若传入的参数包中的参数个数为 1 时,则匹配到该递归终止函数,也就解决递归终止的问题了。
方式二:
递归展开参数包的方式说明:
- 定义一个函数模板,它接收一个参数包
Args
和一个整数index
作为模板参数。 - 在函数模板中,使用
if constexpr
语句对index
是否为 0 进行判断。若index
不为 0,则将递归调用该函数模板,将index
减 1,并将参数包中剩余的参数传递给下一次调用。
示例:
void ShowList() {}
template<class T,class... Args>
void ShowList(T value, Args ...args)
{
if constexpr (sizeof...(args) == 0)
cout << value << endl;
else {
// 递归调用ShowList函数,将参数包中的剩余参数传递给下一次调用
cout << value << " ";
ShowList(args...);
}
}
int main()
{
ShowList();
ShowList(5);
ShowList(3, 3.14, 'C', "template");
return 0;
}
if constexpr 是一种 c++17 语言扩展。
逗号表达式展开参数包
逗号表达式展开参数包是指在函数调用时,使用逗号表达式将多个参数打包成一个参数包,并将该参数包展开为多个独立的此参数传递给函数。
在 c++ 中,逗号表达式可以用于展开参数包。逗号表达式的结果是其最后一个表达式的值。
示例:
// 逗号表达式展开参数包
void ShowList(){}
template<class T>
void PrintArg(T t)
{
cout << t << " ";
}
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args),0)... };
cout << endl;
}
int main()
{
ShowList();
ShowList(5);
ShowList(3, 3.14, 'C', "template");
return 0;
}
这段代码演示了 c++ 中的逗号表达式展开参数包的功能。这段代码中,定义了一个可变参数模板函数 ShowList 和一个辅助函数 PrintArg 。ShowList 函数可以接收任意数量和类型的参数,并将它们展开成一个逗号分隔的列表,最终输出到控制台上面。
- 这种展开参数包的方式,不需要通过递归终止函数,是直接在 expand 函数体中展开的,PrintArg 不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。逗号表达式会按顺序执行逗号前面的表达式。
- expand 函数中的逗号表达式:(PrintArg(args), 0),也是按照这个执行顺序,先执 PrintArg(args) ,再得到逗号表达式的结果 0。
- 同时还用到了 C++11 的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…} 将会展开成 ((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为 0 的数组 int arr[sizeof…(Args)]。
- 由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分 PrintArg(args) 打印出参数,也就是说在构造 in t数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
STL容器中的empalce相关接口函数
c++11 标准对 STL 中的容器增加了 emplace 版本的插入接口,这些接口允许我们在容器中就地构造对象,而不需要手动创建对象并传递给插入函数。
list 容器中的 push_front、push_back、和 insert 函数,都增加了对应的 emplace 版本,如下:
这些 emplace 版本的插入接口支持模板的可变参数,list 容器的声明如下:
因为 emplace 版本接口的可变参数模板参数的类型是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。
- 若调用 emplace 版本接口时传入的是左值对象,那么首先调用构造函数实例化出一个左值对象,然后使用定位 new 表达式调用构造函数对空间进行初始化,将匹配到拷贝构造函数。
- 若调用 emplace 版本接口时传入的是右值对象,那么首先调用构造函数实例化出一个右值对象,然后使用定位 new 表达式调用构造函数对空间进行初始化,将匹配到移动构造函数。
- 如果调用 emplace 版本接口传入的是参数包,那么直接调用函数进行插入,并使用定位 new 表达式调用构造函数对空间进行初始化,将匹配到构造函数。
emplace 版本插入函数的使用示例:
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
emplace 系列接口的意义在于提高代码的效率和可读性,尤其是在需要频繁插入大量元素的情况下,使用它们可以提高代码的性能。