变参模板简介

从C++11标准起,模板参数可以接收任意数量可变的模板实参。这个特性允许在必须传递任意数量和类型的实参的地方使用模板。一个典型的应用是通过类或者框架传递数量和类型都未定的参数,另一个应用是提供泛型代码处理任意数量和类型的参数。

变参模板实例

一个接收任意多参数的print()函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <bitset>
#include <iostream>

void print() {}
template <typename T, typename... Types>
void print(const T &firstArg, const Types &...args)
{
std::cout << firstArg << std::endl;
print(args...);
}

int main()
{
print(7.5, "hello", std::bitset<16>(377), 42);
return 0;
}

如果print函数传入一个或者多个实参,则会调用这个函数模板,它通过单独指定第一个实参,将剩下的实参打包进函数参数包中,递归地调用print函数,将所有实参输出。

为了结束递归调用,在模板之前特化一个无参数的print函数,否则会出现无限递归依赖:recursively required from 'void print(const T&, const Types& ...),这个递归依赖是发生在程序编译期。

变参和非变参模板的重载

如果两个函数模板只存在尾部参数包的差异,则首选没有尾部参数包的函数模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bitset>
#include <iostream>

template <typename T>
void print(const T &arg)
{
std::cout << "print(const T &arg)" << std::endl;
std::cout << arg << std::endl;
}

template <typename T, typename... Types>
void print(const T &firstArg, const Types &...args)
{
std::cout << "print(const T &firstArg, const Types &...args)" << std::endl;
std::cout << firstArg << std::endl;
print(args...);
}

int main()
{
print(7.5);
return 0;
}

print(7.5)优先调用没有变参模板的函数模板。

sizeof…运算符

C++11还为变参模板引入了sizeof运算符的一种新形式:sizeof...他会扩展到参数包所含的参数数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bitset>
#include <iostream>

void print() {}
template <typename T, typename... Types>
void print(const T &firstArg, const Types &...args)
{
std::cout << firstArg << std::endl;
std::cout << "sizeof(Types): " << sizeof...(Types) << std::endl;
print(args...);
}

int main()
{
print(7.5, "hello", std::bitset<16>(377), 42);
return 0;
}

程序输出:

1
2
3
4
5
6
7
8
7.5
sizeof(Types): 3
hello
sizeof(Types): 2
0000000101111001
sizeof(Types): 1
42
sizeof(Types): 0

值得注意的是,我们要清楚,模板的实例化是在编译期完成的,也就是说像下面的例子:

1
2
3
4
5
6
7
8
9
void print(const T &firstArg, const Types &...args)
{
std::cout << firstArg << std::endl;
std::cout << "sizeof(Types): " << sizeof...(Types) << std::endl;
if (sizeof...(Types) > 0)
{
print(args...);
}
}

试图用if语句来判断递归终止条件是行不通的,因为if语句中的两个分支都会被实例化,也就是说,编译期仍会实例化当sizeof...(Types) == 0时的print(args...)函数模板,于是又产生了递归依赖。

折叠表达式

折叠表达式和二元运算符

从C++17开始,有一种特性可以用于计算对一个参数包的所有实参使用二元运算符的结果:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

template <typename... Types>
auto foldSum(const Types &...args)
{
return (... + args);
}
int main()
{
std::cout << foldSum(1, 2, 3, 4, 5) << std::endl;
return 0;
}

其中的+可以替换成任意其他的二元运算符。

当参数包为空时,这种写法通常会报错,但也有几个特例:

xxxxxxxxxx template <typename T1, typename T2>auto sum(T1 a, T2 b) {    return a + b;}cpp 参数包围空时的输出
&& true
|| false
, void()

几乎所有的二元运算符都可以用于折叠表达式,另外,一些运算符组合起来也能达到同二元运算符同样的效果,如使用->*来按某一条指定的路径来遍历二叉树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

struct Node
{
int value;
Node *left;
Node *right;
Node(int value = 0) : value(value), left(nullptr), right(nullptr) {}
};

auto left = &Node::left;
auto right = &Node::right;
template <typename T, typename... Indices>
auto visit(T node, Indices... indices)
{
std::cout << "node->value: " << node->value << std::endl;
return (node->*...->*indices);
}

int main()
{
Node *root = new Node{0};
root->left = new Node{1};
root->right = new Node{2};
root->left->left = new Node{3};
root->left->right = new Node{4};
root->right->left = new Node{5};
root->right->right = new Node{6};

Node *node = visit(root, left, right);
std::cout << "node->value: " << node->value << std::endl;
return 0;
}

xxxxxxxxxx unique_lock &operator=(unique_lock &&__u) noexcept{    if (_M_owns)        unlock();    __u._M_device = 0;    __u._M_owns = false;    unique_lock(std::move(__u)).swap(*this);}cpp

具体规则

折叠表达式 计值
(… op pack) (((pack1 op pack2) op pack3)… op packN)
(pack op …) (pack1 op (…(packN-1 op packN)))
(init op … op pack) (((init op pack1) op pack2)… op packN)
(pack op … op init) (pack1 op (… op (packN op init)))

打印变参参数包

我们可能会想到使用二元运算符<<std::cout来输出一个参数包中的内容:

1
2
3
4
5
6
7
template <typename... Args>
void print(const Args &...args)
{
(std::cout << ... << args) << std::endl;
}
...
print("Hello", "World", 1, 2, 3, 4, 5);

由于<<具有右结合性,所以折叠表达式展开同args1 << args2 << args3 ...

我们发现print函数中的参数全部挤在一起输出出来,如果想给每个参数中添加一个空格作为分格,我们可以构建一个类模板,使用args中的参数构造类模板,将operator<<重载,这样就会调用类模板的<<,再使用std::cout输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
class AddSpace
{
private:
const T &elem;

public:
AddSpace(const T &elem) : elem(elem) {}
friend std::ostream &operator<<(std::ostream &os, const AddSpace<T> &obj)
{
return os << obj.elem << ' ';
}
};

template <typename... Args>
void print(const Args &...args)
{
(std::cout << ... << AddSpace(args)) << std::endl;
}

这个类接收参数包中的一个参数,并重载<<,输出时多输出一个空格达到分隔的目的。注意我们这里并没有显式指定模板参数,在C++17后,若模板类的构造函数可以推导出所有模板参数,那么构造类时就可以不再显式指定。