介绍泛型模板中的可变参数模板。

1. 可变参数模板示例

在C++11以后,模板可以接收任意数量的参数,因此被称作可变参数的模板(variadic templates).

下面展示一段非常精妙的代码:

void print () {} // 没有参数时将调用此函数

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << firstArg << ' ';  // 打印第一个实参
    print(args...); // 调用print()打印其余实参
}

int main()
{
    std::string s("world");
    print(3.14, "hello", s); // 3.14 hello world
}

这段代码采用了递归的办法进行打印,每次递归都会减少参数个数。需要注意

  • Types... args必须在三点后面打空格,有点类似于CV符,引用的那种感觉。
  • args...可以被视为一个打包的参数
  • 每次进入print函数,都会将args...分为一个单一的firstArg和一个新的打包参数args...
  • 必须要准备一个空函数以防止没有参数时将调用此函数

上例也可以如下实现,如果两个函数模板只有尾置参数包不同,会优先匹配没有尾置参数包的版本

template<typename T>
void print (T x)
{
    std::cout << x << ' ';
}

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    print(firstArg);
    print(args...);
}

3. sizeof…

在C++11中,引入了一个新的sizeof...运算符来应对可变参数,它的形式非常有趣:这是一个由标点符号和字符共同组建的一个运算符,它的作用就是算出可变参数的数量

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << sizeof...(args) << '\n'; // print number of remaining args
}

你可能会想到利用这个特性来处理前面的例子中比较麻烦的递归结尾,但这样会报错:实例化后的代码是否能发挥作用是在运行时决定,而实例化的调用是否合法是编译时决定。听起来有点绕,简单来说就是模板编译时,会无视if里面的判断条件,实例化print,直到出现print( )里面为空的情况。然后就是报错。

img

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << firstArg << '\n';
    if (sizeof...(args) > 0) { // sizeof...(args)==0时会出错
        print(args...); // 因为print(args...)仍将被初始化,而此时没有实参
    }
}

4. 折叠表达式

C++17新引入的语法糖,非常有用,值得学习!

折叠表达式的作用就是:使用二元运算符计算剩余参数包里面的参数。举个例子:

template<typename... T>
auto foldSum (T... s)
{
    return (... + s);   // ((s1 + s2) + s3) ...
}

注意一下如果参数包为空,则表达式会被判定为非法。

img

折叠表达式的运算规则如下:

foldSum(1, 2, 3, 4, 5); // 假如实参是12345
(... + s):((((1 + 2) + 3) + 4) + 5)
(s + ...):(1 + (2 + (3 + (4 + 5))))
(0 + ... + s):(((((0 + 1) + 2) + 3) + 4) + 5)
(s + ... + 0):(1 + (2 + (3 + (4 + (5 + 0)))))

除了上面的求和,求累积外,还有其他用法。

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); // root ->* paths1 ->* paths2 ...
}

void main()
{
    Node* node2 = traverse(root, left, right);
    //左子节点,左子节点的右子节点
}

(2)使用折叠表达式简化打印所有参数的可变参数模板

template<typename... Ts>
void print(const Ts&... args)
{
    (std::cout << ... << args) << '\n';
}

如果想用空格分隔参数包元素,需要使用一个包裹类来提供此功能

template<typename T>
class AddSpace {
    const T& ref; // 构造函数中的实参的引用
public:
    AddSpace(const T& r): ref(r) {}
    friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {
        return os << s.ref << ' ';   // 输出传递的实参和一个空格
    }
};

template<typename... Args>
void print(Args... args) {
    (std::cout << ... << AddSpace(args)) << '\n';
}

5. 可变参数表达式

前面说到可变参数的本质也是参数,既然如此我们就可以针对他进行相关的运算。

比如比如让每个元素翻倍后传递给再打印:

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

template<typename... T>
void printDoubled (const T&... args)
{
    print (args + args...);
}

注意参数包的省略号不能直接接在数值字面值后:

template<typename... T>
void addOne(const T&... args)
{
    print (args + 1...); // 错误 1...是带多个小数点的字面值,不合法
    print (args + 1 ...); // OK
    print ((args + 1)...); // OK
}

6. 可变参数索引

以索引形式访问:

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


template<typename C, typename… Idx> 
void printElems (C const& coll, Idx… idx) 
{  
    print (coll[idx]…); 
}

int main()
{
    std::vector<std::string> v{ "good", "times", "say", "bye" };
    printElems(v, 2, 0, 3); // say good bye:等价于print(v[2], v[0], v[3]);
}

非类型模板参数也可以声明为参数包:

template<std::size_t... N, typename C>
void printIdx(const C& c)
{
    print(c[N]...);
}

std::vector<std::string> v{ "good", "times", "say", "bye" };
printIdx<2, 0, 3>(v);