C++高级编程(第4版) 个人笔记 22.5 - Variadic Templates 可变参数模板
参考书籍:
《C++高级编程》(第4版)》
1.看代码粗学.
代码:
#include <iostream>
#include <bitset>
void print(){/*不执行任何操作。*/}
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
std::cout << "args_size: " << sizeof...(args) << std::endl;
std::cout << firstArg << std::endl;
std::cout << "---------------" << std::endl;
print(args...);
}
int main() {
print(10.04, "hello", std::bitset<16>(1124), 629);
return 0;
}
运行结果:
… 就是一个所谓的pack(包)
用于template parameters,就是template parameters pack(模板参数包)
用于function parameter types,就是function parameter types pack(函数参数类型包)
用于function parameters,就是function parameters pack(函数参数类型包)
sizeof… 运算符
查询形参包中的元素数量。
语法;:sizeof…( 形参包 ) (C++11 起)
解释:返回形参包中的元素数量。
2.细节:
普通模板只可采取固定数量的模板参数。可变参数模板(variadic template)可接收可变数目的模板参数。例如,下面的代码定义了一个模板,它可以接收任何数目的模板参数,使用称为Types的参数(parameter pack):
template<typename... Types>
class MyVariadicTemplate {};
注意:
typename之后的三个点并非错误。这是为可变参数模板定义参数包的语法。参数包可以接收可变数目的参数。在三个点的前后允许添加空格。
可用任何数量的类型实例化MyVariadicTemplalte,例如:
MyVariadicTemplate<int> instance1;
MyVariadicTemplate<std::string, double, std::list<int>> instance2;
甚至可用零个模板参数实例化MyVariadicTemplalte:
MyVariadicTemplate<> instance3;
为避免用零个模板参数实例化可变参数模板,可以像下面这样编写模板:
template<typename T1,typename... Types>
class MyVariadicTemplate {};
有了这个定义后,试图通过零个模板参数实例化MyVariadicTemplate会导致编译错误。例如,JetBrains CLion会给出如下错误:
//Error: wrong number of template arguments (0, should be at least 1)
不能直接遍历传给可变参数模板的不同参数。唯一方法是借助模板递归的帮助。下面通过两个例子来说明如何使用可变参数模板。
2.1类型安全的变长参数列表.
可变参数模板允许创建类型安全的变长参数列表。下面的例子定义了一个可变参数processValues(),它允许以类型安全的方式接收不同类型的可变数目的参数。函数processValues() 会处理变长参数列表中的每个值,对每个参数执行 handleValue() 函数。这意味着必须对每种要处理的类型编写handleValue() 函数,例如下例中的int、double 和string:
#include <iostream>
using namespace std;
void handleValue(int value) {
cout << "Integer: " << value << endl;
}
void handleValue(double value) {
cout << "Double: " << value << endl;
}
void handleValue(string_view value) {
cout << "String: " << value << endl;
}
void processValues(){/*不执行任何操作。*/}
template<typename T1, typename... Tn>
void processValues(T1 arg1, Tn... args) {
handleValue(arg1);
processValues(args...);
}
int main() {
processValues(1, 2, 3.1415926, "test_string", 1.1f);
}
在前面的例子中,三点运算符“…”用了两次。这个运算符出现在3个地方,有两个不同的含义。首先用在模板参数列表中typename的后面以及函数参数列表中类型Tn的后面。在这两种情况下,它都表示参数包。参数包可接收可变数目的参数。
… 运算符的第二种用法是在函数体中参数名args的后面。这种情况下,它表示参数包扩展。 这个运算符会解包展开参数包,得到各个参数。它基本上提取出运算符左边的内容,为包中的每个模板参数重复该内容,并用逗号隔开。从前面的例子中取出以下行:
processValues(args...) ;
这一行将args参数包解包(或扩展)为不同的参数,通过逗号分隔参数,然后用这些展开的参数调用processValues() 函数。模板总是需要至少一个模板参数: T1。通过arg… 递归调用 processValues() 的结果是:每次调用都会少一个模板参数。
由于processValues() 函数的实现是递归的,因此需要采用一种方法来停止递归。为此,实现一个processValues() 函数,要求它接收零个参数。
测试结果:
这个例子生成的递归调用是:
processValues(1, 2, 3.1415926, "test_string", 1.1f);
handleValue(1);
processValues(2, 3.1415926, "test_string", 1.1f);
handleValue(2);
processValues(3.1415926, "test_string", 1.1f);
handleValue(3.1415926);
processValues( "test_string", 1.1f);
handleValue("test_string");
processValues( 1.1f);
handleValue(1.1f);
processValues();
重要的是要记住,这种变长参数列表是完全类型安全的。processValues() 函数会根据实际类型自动调用正确的handleValue() 重载版本。C++中 也会像通常那样自动执行类型转换。例如,前面例子中1.1f的类型为float。processValues() 函数会调用handleValue(double value), 因为从float到double的转换没有任何损失。然而,如果调用 processValues() 时带有某种类型的参数,而这种类型没有对应的handleValue() 函数,编译器会产生错误。
前面的实现存在一个小问题。由于这是一个递归的实现,因此每次递归调用processValues() 时都会复制参数。根据参数的类型,这种做法的代价可能会很高。你可能会认为,向processValues() 函数传递引用而不使用按值传递方法,就可以避免这种复制问题。遗憾的是,这样也无法通过字面量调用processValues() 了,因为不允许使用字面量引用,除非使用const引用。
为了在使用非const引用的同时也能使用字面量值,可使用转发引用(forwarding references)。以下实现使用了转发引用T&&,还使用st::forward() 完美转发所有”意味着,如果把 rvalue传递给processValues(),就将它作为rvalue引用转发;如果把lvalue或Ivalue引用传递给processValues(),就将它作为lvalue引用转发。
void processValues(){/*不执行任何操作。*/}
template<typename T1, typename... Tn>
void processValues(T1&& arg1, Tn&&... args) {
handleValue(std::forward<T1>(arg1));
processValues(std::forward<Tn>(args)...);
}
std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
有一行代码需要做进一步解释:
processValues(std::forward<Tn>(args)...);
“ … ”运算用于解开参数包,它在参数包中的每个参数上使用std::forward用逗号把它们隔开。例如,假设args是一个参数包,有三个参数(a1、a2 和 a3),分别对应三种类型(A1、A2 和 A3)。扩展后的调用如下:
processValues(std::forward<Al>(a1),
std::forward<A2> (a2),
std:: forward<A3> (a3)) ;
在使用了参数包的函数体中,可通过以下方法获得参数包中参数的个数:
int numOfArgs = sizeof... (args) ;
一个使用变长参数模板的实际例子是编写一个类似于 printf() 版本的安全且类型安全的函数模板。这是实践变长参数模板的一次不错练习。
欢迎关注公众号:c_302888524
发送:“C++高级编程(第3版)” 获取电子书