可变参数模板 Variadic Templates
从 C++11 开始,模板可以有任意个模板参数,也即所谓的可变参数模板。
可变参数模板
可变参数模板的例子
定义一个函数 print()
如下,用于接收不同类型的不同参数。
#include <iostream>
void print ()
{
}
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << '\n'; // print first argument
print(args...); // call print() for remaining arguments
}
typename... Args
被称为模板参数包(template parameter pack),Args... args
被称为函数参数包(function parameter pack)。模板函数 void print (T firstArg, Types... args)
首先打印第一个参数,然后递归调用自己打印剩余参数。为了结束递归,需要提供一个非模板类型重载函数,用于处理最后的空参数包。
为了更直观地感受可变参数模板地递归处理过程,可以在以上 print
中插入 __PRETTY_FUNCTION__
,打印出函数的调用。
#include <iostream>
void print ()
{
std::cout << __PRETTY_FUNCTION__ << "\n";
}
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << '\n'; // print first argument
std::cout << __PRETTY_FUNCTION__ << "\n";
print(args...); // call print() for remaining arguments
}
当发生 print (7.5, "hello", 5)
调用时,输出如下:
7.5
void print(T, Types ...) [with T = double; Types = {const char*, int}]
hello
void print(T, Types ...) [with T = const char*; Types = {int}]
5
void print(T, Types ...) [with T = int; Types = {}]
void print()
重载可变和非可变参数模板
也可以实现上述例子如下:
#include <iostream>
template<typename T>
void print (T arg)
{
std::cout << arg << '\n'; // print passed argument
}
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
print(firstArg); // call print() for the first argument
print(args...); // call print() for remaining arguments
}
两个 ptint
的区别在于拖尾参数包,没有拖尾参数包的函数会被更优先匹配。
sizeof… 操作符
C++ 引入操作 sizeof...
用于计算可变参数包中元素的个数。
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << sizeof...(Types) << '\n'; // print number of remaining types
std::cout << sizeof...(args) << '\n'; // print number of remaining args
...
}
添加以上两次 sizeof...
的调用,可以打印每次调用 print
时模板参数包和函数参数包元素的个数。
你可能会认为,借助 sizeof...
,可以省去一个空参数的 print
:
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << '\n';
if (sizeof...(args) > 0) { // error if sizeof...(args)==0
print(args...); // and no print() for no arguments declared
}
}
但是,上面的代码无法工作。模板代码的实例是编译期决定的,而实例化的代码是否被执行是运行期决定的。也即 if
语句在编译期会生成,由于没有空参数的 print()
而导致编译失败。
在 C++17 开始支持编译期的 if
语句:
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << '\n';
if constexpr (sizeof...(args) > 0) { // error if sizeof...(args)==0
print(args...); // and no print() for no arguments declared
}
}
折叠表达式
C++17 引入了折叠表达式(Fold Expressions),用于对参数包中参数进行二元计算。例如:
template<typename... T>
auto foldSum (T... s) {
return (... + s); // ((s1 + s2) + s3) ...
}
则 auto ret = foldSum(3, 4.5, 9.0);
得到 16.5
。
如果参数包为空,表达式通常是非法的。但是空参数包二元计算的例外情况:&&
视为 true
,||
视为 false
,逗号运算符视为 void()
。
可能的折叠表达式如下表:
折叠表达式 | 计算结果 |
---|---|
(… 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 (… (packN op init))) |
折叠表达式几乎可以使用所有二元运算符。例如,可以使用运算符 ->*
遍历一个二叉树的路径:
#include <cassert>
struct Node {
int val;
Node* left;
Node* right;
Node(int i = 0) : val(i), left(nullptr), right(nullptr) {}
};
template <typename T, typename... Ts>
Node* traverse(T root, Ts... paths) {
return (root->*...->*paths); // np ->* paths1 ->* paths2 ...
}
int main() {
Node* root = new Node{0};
root->left = new Node{1};
root->left->right = new Node{2};
root->left->right->left = new Node{3};
auto left = &Node::left;
auto right = &Node::right;
Node* node1 = traverse(root, left);
assert(node1->val == 1);
Node* node2 = traverse(root, left, right);
assert(node2->val == 2);
Node* node3 = traverse(node2, left);
assert(node3->val == 3);
}
使用折叠表达式,我们可以简化前面的可变参数模板 print
的实现:
#include <iostream>
template<typename... Types>
void print (Types const&... args)
{
(std::cout << ... << args) << '\n';
}
int main() {
print(3, 4.5, 5); //34.55
}
但是,上面的打印没有空白分割符来分割不同的参数。为了增加空白分隔符,需要增加一个类模板来辅助实现:
#include <iostream>
template<typename T>
class AddSpace
{
private:
T const& ref; // refer to argument passed in constructor
public:
AddSpace(T const& r): ref(r) {
}
friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {
return os << s.ref << ' '; // output passed argument and a space
}
};
template<typename... Types>
void print (Types const&... args)
{
(std::cout << ... << AddSpace(args)) << '\n';
}
int main() {
print(3, 4.5, 5); // 3 4.5 5
}
可变参数模板的应用
可变参数模板在 C++ 标准库中扮演重要角色。可变参数模板的典型应用是转发任意数量、任意类型的实参。例如:
// create shared pointer to complex<float> initialized by 4.2 and 7.7:
auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);
std::thread t (foo, 42, "hello"); // call foo(42,"hello") in a separate thread
std::vector<Customer> v;
...
v.emplace("Tim", "Jovi", 1962); // insert a Customer initialized by three arguments
通常,这些参数会进行完美转发,在标准库中申明如下:
namespace std {
template <typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args);
class thread {
public:
template <typename F, typename... Args>
explicit thread(F&& f, Args&&... args);
...
};
template <typename T, typename Allocator = allocator<T>>
class vector {
public:
template <typename... Args>
reference emplace_back(Args&&... args);
...
};
}
需要注意的是:和普通函数一样,可变参函数模板的参数有一样应用的规则。例如,值传参时,参数会被拷贝和退化(例如,数组会退化成指针);引用传参时,入参指向原始的参数并且不会退化:
// args are copies with decayed types:
template<typename... Args> void foo (Args... args);
// args are nondecayed references to passed objects:
template<typename... Args> void bar (Args const&... args);
可变参数类模板和可变表达式
除了上述例子,参数包还可以出现在其他地方,例如表达式、类模板、使用申明、类型推导指南。
可变表达式
不仅可以转发参数,还可以计算参数。例如:
template<typename... T>
void printDoubled (T const&... args)
{
print (args + args...);
}
例如,调用如下:
printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));
相当于如下效果:
print(7.5 + 7.5,
std::string("hello") + std::string("hello"),
std::complex<float>(4,2) + std::complex<float>(4,2);
如果只想给每个参数加1,请注意省略号中的点可能不会直接跟在数字文字后面。
template<typename... T>
void addOne (T const&... args)
{
print (args + 1...); // ERROR: 1... is a literal with too many decimal points
print (args + 1 ...); // OK
print ((args + 1)...); // OK
}
编译期的表达式也可以包含在模板参数中。例如下面的函数模板用于返回所以参数类型是否相同:
template<typename T1, typename... TN>
constexpr bool isHomogeneous (T1, TN...)
{
return (std::is_same<T1,TN>::value && ...); // since C++17
}
这是折叠表达式的应用。对于:
isHomogeneous(43, -1, "hello")
会被扩展为
std::is_same<int,int>::value && std::is_same<int,char const*>::value
并且返回 false
。而
isHomogeneous("hello", " ", "world", "!")
则返回 true
。因为所有的参数被推导为 char*
(值传参,参数类型退化)。
可变下标
另一个应用:下面的函数使用可变的下标列表访问第一个参数的相应元素。
template<typename C, typename... Idx>
void printElems (C const& coll, Idx... idx)
{
print (coll[idx]...);
}
例如:
std::vector<std::string> coll = {"good", "times", "say", "bye"};
printElems(coll,2,0,3);
效果相当于调用
print (coll[2], coll[0], coll[3]);
还可以将非类型模板参数申明为参数包。例如:
template<std::size_t... Idx, typename C>
void printIdx (C const& coll)
{
print(coll[Idx]...);
}
例如:
std::vector<std::string> coll = {"good", "times", "say", "bye"};
printIdx<2,0,3>(coll);
可变参数类模板
可变参数模板也可以是类模板。一个重要的例子就是任意参数、任意类型的类 tuple
。
template<typename... Elements>
class Tuple;
Tuple<int, std::string, char> t; // t can hold integer, string, and character
std::tuple
将在后续章节继续讨论。另一个例子是 std::variant
:
template<typename... Types>
class Variant;
Variant<int, std::string, char> v; // v can hold integer, string, or character
还可以定义一个类表示下标的类型:
// type for arbitrary number of indices:
template<std::size_t...>
struct Indices {
};
template<typename T, std::size_t... Idx>
void printByIdx(T t, Indices<Idx...>)
{
print(std::get<Idx>(t)...);
}
std::array<std::string, 5> arr = {"Hello", "my", "new", "!", "World"};
printByIdx(arr, Indices<0, 4, 3>());
这是元编程的第一步,这将在后续章节继续讨论。
可变参数推导指南
甚至,推导指南也可以是可变参数。例如:
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 a{42,45,77}; // 推导为 std::array<int, 3>
推导指南这里使用了折叠表达式,相当于
is_same_v<T, U1> && is_same_v<T, U2> && is_same_v<T, U3> ...
如果表达式不为 true
,整个推导将会失败。
可变参数基类和 using
最后看一个例子:
#include <string>
#include <unordered_set>
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 {
std::size_t operator() (Customer const& c) const {
return std::hash<std::string>()(c.getName());
}
};
// define class that combines operator() for variadic base classes:
template<typename... Bases>
struct Overloader : Bases...
{
using Bases::operator()...; // OK since C++17
};
int main()
{
// combine hasher and equality for customers in one type:
using CustomerOP = Overloader<CustomerHash,CustomerEq>;
std::unordered_set<Customer,CustomerHash,CustomerEq> coll1;
std::unordered_set<Customer,CustomerOP,CustomerOP> coll2;
...
}
这里 ,首先定义了一个类 Customer
和 两个函数对象 CustomerEq
、CustomerHash
。然后 定义 Overloader
,继承于可变参数的基类。通过如下申明,CustomerOP
可以使能两个基类的 operator()
的实现。
using CustomerOP = Overloader<CustomerHash,CustomerEq>;
后续章节将继续讨论该技术的应用。
参考
- http://www.tmplbook.com