可变参数模板示例

  1. void print() {} // 没有参数时将调用此函数
  2. template<typename T, typename... Types>
  3. void print(T firstArg, Types... args)
  4. {
  5. std::cout << firstArg << ' '; // 打印第一个实参,无参数时将调用此函数
  6. print(args...); // 调用print()打印其余实参
  7. }
  8. int main()
  9. {
  10. std::string s("world");
  11. print(3.14, "hello", s); // 3.14 hello world
  12. }

重载可变参数和非可变参数模板

  • 前一例子也可以如下实现,如果两个函数模板只有尾置参数包不同,会优先匹配没有尾置参数包的版本 ```cpp template void print(T x) { std::cout << x << ‘ ‘; }

template void print(T firstArg, Types… args) { print(firstArg); print(args…); }

  1. <a name="cfed3f94"></a>
  2. ## sizeof...运算符
  3. - [sizeof...](https://zh.cppreference.com/w/cpp/language/sizeof...)用于计算参数包的元素数
  4. ```cpp
  5. template<typename T, typename... Types>
  6. void print(T firstArg, Types... args)
  7. {
  8. std::cout << sizeof...(Types) << '\n'; // print number of remaining types
  9. std::cout << sizeof...(args) << '\n'; // print number of remaining args
  10. }
  • 可能会想到将其用于跳过递归结尾,以防缺少实参,但这是错误的

    1. template<typename T, typename... Types>
    2. void print(T firstArg, Types... args)
    3. {
    4. std::cout << firstArg << '\n';
    5. if (sizeof...(args) > 0) // sizeof...(args)==0时会出错
    6. {
    7. print(args...); // 因为print(args...)仍将被初始化,而此时没有实参
    8. }
    9. }
  • 函数模板中所有的if语句分支都会被实例化,上面代码中当对最后一个实参调用print()时,打印了实参后,sizeof...(args)为0,但没有实参时print(args…)仍然会初始化,结果就会出错。C++17中引入了编译期if来解决这个问题

    1. template<typename T, typename...Types>
    2. void print(const T& firstArg, const Types&... args)
    3. {
    4. std::cout << firstArg << '\n';
    5. if constexpr (sizeof...(args) > 0)
    6. {
    7. print(args...); // 只在sizeof...(args) > 0时实例化
    8. }
    9. }

折叠表达式

  • C++17引入了折叠表达式,用于获取对所有参数包实参使用二元运算符的计算结果。如下模板将返回所有实参的和

    1. template<typename... T>
    2. auto foldSum(T... s)
    3. {
    4. return (... + s); // ((s1 + s2) + s3) ...
    5. }
  • 如果参数包为空,表达式通常是非法的(对空参数包例外的是:&&视为true,||视为false,逗号运算符视为void())

图片.png

  • 上例中的折叠表达式还可以有如下形式

    1. foldSum(1, 2, 3, 4, 5); // 假如实参是12345
    2. // 左边是返回值,右边是计算时的内部展开方式
    3. (... + s):((((1 + 2) + 3) + 4) + 5)
    4. (s + ...):(1 + (2 + (3 + (4 + 5))))
    5. (0 + ... + s):(((((0 + 1) + 2) + 3) + 4) + 5)
    6. (s + ... + 0):(1 + (2 + (3 + (4 + (5 + 0)))))
  • 折叠表达式几乎可以使用所有二元运算符 ```cpp struct Node { int val; Node left; Node right; Node(int i = 0) : val(i), left(nullptr), right(nullptr) {} };

// 使用operator->的折叠表达式,用于遍历指定的二叉树路径 template 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); std::cout << node1->val; // 1 Node node2 = traverse(root, left, right); std::cout << node2->val; // 2 Node* node3 = traverse(node2, left); std::cout << node3->val; // 3 }

  1. - 使用折叠表达式简化打印所有参数的可变参数模板
  2. ```cpp
  3. template<typename... Ts>
  4. void print(const Ts&... args)
  5. {
  6. (std::cout << ... << args) << '\n';
  7. }
  • 如果想用空格分隔参数包元素,需要使用一个包裹类来提供此功能 ```cpp template class AddSpace { public: AddSpace(const T& r): ref(r) {} friend std::ostream& operator<<(std::ostream& os, AddSpace s) { return os << s.ref << ‘ ‘; // 输出传递的实参和一个空格 } private: const T& ref; // 构造函数中的实参的引用 };

template void print(Args… args) { (std::cout << … << AddSpace(args)) << ‘\n’; }

  1. <a name="c9ef3559"></a>
  2. ## 可变参数模板的应用
  3. - 可变参数模板的典型应用是转发任意数量任意类型的实参,比如[std::make_shared](https://zh.cppreference.com/w/cpp/memory/shared_ptr/make_shared)就是用它实现的
  4. ```cpp
  5. auto p = std::make_shared<std::complex<double>>(3.14, 4.2);

std::vector v; v.emplace_back(1, “hi”);

  1. - 通常这类实参会使用移动语义进行完美转发,上述例子在标准库中对应的声明如下
  2. ```cpp
  3. namespace std {
  4. template<typename T, typename... Args>
  5. shared_ptr<T> make_shared(Args&&... args);
  6. class thread {
  7. public:
  8. template<typename F, typename... Args>
  9. explicit thread(F&& f, Args&&... args);
  10. ...
  11. };
  12. template<typename T, typename Allocator = allocator<T>>
  13. class vector {
  14. public:
  15. template<typename... Args>
  16. reference emplace_back(Args&&... args);
  17. ...
  18. };
  19. }
  • 除了上述例子,参数包还能用于其他地方,如表达式、类模板、using声明、deduction guide

可变参数表达式

  • 可以对参数包中的参数进行运算,比如让每个元素翻倍后传递给再打印 ```cpp template void print(const Args&… args) { (std::cout << … << args); }

template void printDoubled(const T&… args) { print(args + args…); }

int main() { printDoubled(3.14, std::string(“hi”), std::complex(4, 2)); // 等价于 print(3.14 + 3.14, std::string(“hi”) + std::string(“hi”), std::complex(4, 2) + std::complex(4, 2)); }

  1. - 注意参数包的省略号不能直接接在数值字面值后
  2. ```cpp
  3. template<typename... T>
  4. void addOne(const T&... args)
  5. {
  6. print(args + 1...); // 错误 1...是带多个小数点的字面值,不合法
  7. print(args + 1 ...); // OK
  8. print((args + 1)...); // OK
  9. }
  • 编译期表达式能以同样的方式包含模板参数包 ```cpp template constexpr bool isHomogeneous(T1, TN…) { // 判断是否所有实参类型相同 return (std::is_same_v && …); // since C++17 }

isHomogeneous(1, 2, “hi”); // 结果为false // 扩展为std::is_same_v && std::is_same_v isHomogeneous(“hello”, “”, “world”, “!”) // 结果为true:所有实参都为const char*

  1. <a name="98a4ba7a"></a>
  2. ### 可变参数索引
  3. - 下面函数使用一个可变索引列表访问传递的第一个实参对应的元素
  4. ```cpp
  5. template<typename... Args>
  6. void print(const Args&... args)
  7. {
  8. (std::cout << ... << args);
  9. }
  10. template<typename C, typename... N>
  11. void printElems(const C& c, N... n)
  12. {
  13. print(c[n]...);
  14. }
  15. int main()
  16. {
  17. std::vector<std::string> v{ "good", "times", "say", "bye" };
  18. printElems(v, 2, 0, 3); // say good bye:等价于print(v[2], v[0], v[3]);
  19. }
  • 非类型模板参数也可以声明为参数包 ```cpp template void printIdx(const C& c) { print(c[N]…); }

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

  1. <a name="3bcf427d"></a>
  2. ### 可变参数类模板
  3. - 可变参数类模板的一个重要例子是[std::tuple](https://zh.cppreference.com/w/cpp/utility/tuple)
  4. ```cpp
  5. template<class... Types>
  6. class tuple;
  7. tuple<int, std::string, char> t;
  • 另一个例子是std::variant ```cpp template class variant;

variant v;

  1. - 也能定义一个类作为表示一个索引列表的类型
  2. ```cpp
  3. template<std::size_t...>
  4. struct Indices
  5. {};
  6. template<typename... Args>
  7. void print(const Args&... args)
  8. {
  9. (std::cout << ... << args);
  10. }
  11. template<typename T, std::size_t... N>
  12. void printByIdx(T t, Indices<N...>)
  13. {
  14. print(std::get<N>(t)...);
  15. }
  16. int main()
  17. {
  18. std::array<std::string, 5> arr{ "Hello", "my", "new", "!", "World" };
  19. printByIdx(arr, Indices<0, 4, 3>()); // HelloWorld!
  20. auto t = std::make_tuple(12, "monkeys", 2.0);
  21. printByIdx(t, Indices<0, 1, 2>()); // 12monkeys2
  22. }

可变参数推断指南

  • C++17的标准库中对std::array定义了如下deduction guide
    1. namespace std {
    2. template<typename T, typename... U> array(T, U...)
    3. -> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
    4. }

可变参数基类与using

  1. class A {
  2. public:
  3. A(const std::string& x) : s(x) {}
  4. auto f() const { return s; }
  5. private:
  6. std::string s;
  7. };
  8. struct A_EQ {
  9. bool operator() (const A& lhs, const A& rhs) const
  10. {
  11. return lhs.f() == rhs.f();
  12. }
  13. };
  14. struct A_Hash {
  15. std::size_t operator() (const A& a) const
  16. {
  17. return std::hash<std::string>{}(a.f());
  18. }
  19. };
  20. // 定义一个组合所有基类的operator()的派生类
  21. template<typename... Bases>
  22. struct Overloader : Bases...
  23. {
  24. using Bases::operator()...; // OK since C++17
  25. };
  26. int main()
  27. {
  28. // 将A_EQ和A_Hash组合到一个类型中
  29. using A_OP = Overloader<A_Hash, A_EQ>;
  30. /* unordered_set的声明
  31. template<
  32. class Key,
  33. class Hash = std::hash<Key>,
  34. class KeyEqual = std::equal_to<Key>,
  35. class Allocator = std::allocator<Key>
  36. > class unordered_set;
  37. */
  38. std::unordered_set<A, A_Hash, A_EQ> s1;
  39. std::unordered_set<A, A_OP, A_OP> s2;
  40. }