C++ 模板022-可变模板

自 C++ 11, 模板可以接受可变数量的参数,这一特性允许向模板传入任意数目的任意类型参数

可变函数模板

例如,可以定义如下函数以实现任意类型任意参数的输出:

1
2
3
4
5
6
void print() {}
template<typename T, Typename... Args>
void print(T t, Args... args){
std::cout << t << std::endl;
print(args...);
}

当传入一个或多个参数时,参数列表中的第一个参数将匹配到 T t, 其余参数则被聚合在 Args... args 中,称为模板参数包.

print 模板内, 函数会递归地解包模板参数包并执行实例化的函数

当所有参数都解包完成时, 空的参数列表会匹配到 print 的非模板实现, 以终止递归

重载可变参数模板

要实现可变参数模板的递归终止, 也可以为其重载一个非可变参数模板:

1
2
3
4
5
6
7
8
9
template<typename T>
void print(T t){
std::cout << t << std::endl;
}
template<typename T, typename... Args>
void print(T t, Args... args){
print(t);
print(args...);
}

这是因为, 当两个函数模板仅区别于可变参数包时, 非可变参数模板将优先被决议. 因此当参数解包到只剩下一个参数时, 非可变参数模板将优先被匹配

可变参数模板的 sizeof 运算符

C++ 11 为模板参数包引入了新的 sizeof 运算符:

1
2
3
4
5
template<typename T, typename... Ts>
void size(T t, Ts... ts){
sizeof...(Ts); // number of types in Ts
sizeof...(ts); // number of values in ts
}

fold expressions

fold expressions 是 C++ 17 引入的一项新特性,它允许使用二元运算符在模板参数包中的所有参数上计算结果

例如如下的函数模板:

1
2
3
4
template<typename... Ts>
auto foldSum(Ts... ts){
return (... + ts);
}

其效果是从 ts 中的第一个参数加到其最后一个参数,即:

1
(((ts1 + ts2) + ... )+ tsn)

注意如果传入的参数包为空,那么 fold expression 通常不会通过编译(ill-formed)。但对于以下操作符,其可以返回确定的结果:

operator result
&& true
`
, void()

fold expression 的所有形式:

fold expression evaluation
(... op pack) (((pack1 op pack2) op pack3) ... op packN)
(pack op ...) (pack1 op (... 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)))

可变类模板和其他可变表达式

可变参数包可以出现在:

  • 表达式
  • 类模板
  • using 声明
  • deduction guides

可变表达式

一些例子:

  1. 打印翻倍后的参数:
1
2
3
4
template<typename... T>
void printDouble(T... args){
print(args + args...);
}

效果相当于:

1
2
3
4
5
print(
arg1 + arg1,
arg2 + arg2,
...
);

注意到此时 print 也应该是一个可变参数模板

  1. 打印加 1 后的参数:
1
2
3
4
5
template<typename... T>
void printPlusOne(T... args){
print(args + 1 ...);
// or: print((args + 1)...);
}

相当于:

1
2
3
4
5
print(
arg1 + 1,
arg2 + 1,
...
);

注意到此时 ... 符号不能直接跟在数字 1 后面,需要空开一个空格

  1. 编译时表达式:
1
2
3
4
template<typename T1, typename... TN>
constexpr bool isHoMogeneous(T1, TN...){
return (std::is_same_v<T1, TN> && ...);
}

可变索引

利用可变表达式,可以实现对任意数量的数组索引访问的函数:

1
2
3
4
template<typename Container, typename... Idx>
void printElems(Container const& c, Idx... idx){
print(c[idx]...);
}

注意到此时作为数组索引的参数 idx 可以是任意数量和任意类型,也可以将其声明为非类型模板参数以限制其类型:

1
2
3
4
template<std::size_t... idx, typename Container>
void printElems(Container const& c){
print(c[idx]...);
}

可以以下面的形式调用该函数:

1
printElems<1, 2, 3, 5>(c); // print(c[1], c[2], c[3], c[5]); 

可变类模板

  1. 元组类

元组类需要能够持有任意数量任意类型的值:

1
2
template<typename... Elems>
class Turple;
  1. Variant

Variant 对象可以持有预先声明的任意类型的值:

1
2
template<typename... Elems>
class Variant;
  1. 表示任意下标列表的类型
1
2
template<std::size_t... idx>
struct Index {};

此类型可以将传入的下标表示为列表, 但是其并未实际(内存上)持有这些下标值, 毕竟该类型定义为一个空的 struct

它的表示功能实际上发生在编译期, 当该类型的对象作为参数传入函数时, 实际上是某种类型萃取功能实现了 Idx 的传递:

1
2
3
4
template<typename Container, std::size_t... Idx>
void printByIdx(Container const& c, Index<Idx...>){ // trait Idx out
print(c[Idx]...);
}

variadic deduction guides

对于接受可变参数的函数模板,当其需要返回值deduction guide 时,deduction guide 也可以是可变表达式。

例如 STL 中对 std::array 构造函数的声明:

1
2
3
4
5
6
namespace std {
template<typename T,typename ... U>
array(T, U...) -> array<
enable_if_t<(is_same_v<T, U> && ...), T>,
(1 + sizeof...(U))>;
}

此声明中为 std::array 定义了返回值 deduction guide, 并在其中使用了 enable_if, 以实现当构造函数传入的初始值类型不同时禁用构造函数.

在构造函数传入的可变参数包的类型判断上,该 deduction guide 使用了可变表达式。 同时在参数包数量的计算上,使用了用于可变参数包的 sizeof 操作符

可变基类和 using 声明

在继承体系中基类也可以是可变的,即,可以通过参数包实现对任意数量任意类型的继承。

例如:

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 <string>
#include <unordered_map>

class Customer {
private:
std::string name;
public:
Customer(std::string const& n) : name(n) { }
std::string getName() const { return name; }
};

struct CustomerEq {
bool operator() (Customer const& c1, Customer const& c2) const {
return c1.getName() == c2.getName();
}
};

struct CustomerHash {
bool operator() (Customer const& c) const {
return std::hash<std::string>(c.getName());
}
};

template<typename... Bases>
struct Overloader : Bases... {
using Bases::operator()...;
};

int main() {
using CustomOps = Overloader<CustomerEq, CustomerHash>;
std::unordered_set<Customer, CustomerHash, CustomerEq> s1;
std::unordered_set<Customer, CustomOps, CustomOps> s2;
}

该例中,CustomOps 通过可变基类同时继承了 CustomerEqCustomerHash 这两个函数对象, 并且在定义中使用可变 using 声明同时启用了两个基类的 () 操作符

此时的 CustomOps 同时具备了 CustomerEqCustomerHash 这两个函数对象的功能. 即通过 CustomOps 调用 () 操作符时,其会根据参数自动匹配基类的定义。

因此,在 main 函数中定义的两个自定义相等和哈希操作的 unordered_set 能够实现相同的功能。

可变参数模板一些场景

可变参数模板在标准模板库中应用广泛, 主要用途体现在参数转发上

  • 在一些资源管理类中其可用于转发任意数量任意类型的参数到被管理对象的构造函数。 如:
1
auto share_ptr = std::make_shared<std::complex<float>>(1.1, 2.2);
  • 将可调用对象的参数传递进 thread 对象:
1
std::thread t(f, 1, "hello"); // call f(1, "hello") in thread t
  • 在容器中接收构造对象所需的参数并直接构造对象(emplace):
1
2
std::vector<std::pair<int, int>> vec;
vec.emplace_back(1, 2); // construct std::pair<int, int> {1, 2} in vec

对于传递参数的函数, 其参数通常被声明为万能引用的形式.