• 可变参数模板 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    当发生 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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    也可以实现上述例子如下:

    #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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    两个 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
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    添加以上两次 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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但是,上面的代码无法工作。模板代码的实例是编译期决定的,而实例化的代码是否被执行是运行期决定的。也即 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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    折叠表达式

    C++17 引入了折叠表达式(Fold Expressions),用于对参数包中参数进行二元计算。例如:

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

    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);
    }
    
    • 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

    使用折叠表达式,我们可以简化前面的可变参数模板 print 的实现:

    #include <iostream>
    
    template<typename... Types>
    void print (Types const&... args)
    {
      (std::cout << ... << args) << '\n';
    }
    
    int main() {
      print(3, 4.5, 5);  //34.55
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是,上面的打印没有空白分割符来分割不同的参数。为了增加空白分隔符,需要增加一个类模板来辅助实现:

    #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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可变参数模板的应用

    可变参数模板在 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通常,这些参数会进行完美转发,在标准库中申明如下:

    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);
      ...
    };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    需要注意的是:和普通函数一样,可变参函数模板的参数有一样应用的规则。例如,值传参时,参数会被拷贝和退化(例如,数组会退化成指针);引用传参时,入参指向原始的参数并且不会退化:

    // 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);
    
    • 1
    • 2
    • 3
    • 4

    可变参数类模板和可变表达式

    除了上述例子,参数包还可以出现在其他地方,例如表达式、类模板、使用申明、类型推导指南。

    可变表达式

    不仅可以转发参数,还可以计算参数。例如:

    template<typename... T>
    void printDoubled (T const&... args)
    {
      print (args + args...);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    例如,调用如下:

    printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));
    
    • 1

    相当于如下效果:

    print(7.5 + 7.5,
          std::string("hello") + std::string("hello"),
          std::complex<float>(4,2) + std::complex<float>(4,2);
    
    • 1
    • 2
    • 3

    如果只想给每个参数加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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编译期的表达式也可以包含在模板参数中。例如下面的函数模板用于返回所以参数类型是否相同:

    template<typename T1, typename... TN>
    constexpr bool isHomogeneous (T1, TN...)
    {
      return (std::is_same<T1,TN>::value && ...); // since C++17
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这是折叠表达式的应用。对于:

    isHomogeneous(43, -1, "hello")
    
    • 1

    会被扩展为

    std::is_same<int,int>::value && std::is_same<int,char const*>::value
    
    • 1

    并且返回 false。而

    isHomogeneous("hello", " ", "world", "!")
    
    • 1

    则返回 true。因为所有的参数被推导为 char*(值传参,参数类型退化)。

    可变下标

    另一个应用:下面的函数使用可变的下标列表访问第一个参数的相应元素。

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

    例如:

    std::vector<std::string> coll = {"good", "times", "say", "bye"};
    printElems(coll,2,0,3);
    
    • 1
    • 2

    效果相当于调用

    print (coll[2], coll[0], coll[3]);
    
    • 1

    还可以将非类型模板参数申明为参数包。例如:

    template<std::size_t... Idx, typename C>
    void printIdx (C const& coll)
    {
      print(coll[Idx]...);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    例如:

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

    可变参数类模板

    可变参数模板也可以是类模板。一个重要的例子就是任意参数、任意类型的类 tuple

    template<typename... Elements>
    class Tuple;
    Tuple<int, std::string, char> t; // t can hold integer, string, and character
    
    • 1
    • 2
    • 3

    std::tuple 将在后续章节继续讨论。另一个例子是 std::variant

    template<typename... Types>
    class Variant;
    Variant<int, std::string, char> v; // v can hold integer, string, or character
    
    • 1
    • 2
    • 3

    还可以定义一个类表示下标的类型:

    // 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>());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这是元编程的第一步,这将在后续章节继续讨论。

    可变参数推导指南

    甚至,推导指南也可以是可变参数。例如:

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    推导指南这里使用了折叠表达式,相当于

    is_same_v<T, U1> && is_same_v<T, U2> && is_same_v<T, U3> ...
    
    • 1

    如果表达式不为 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;
      ...
    }
    
    • 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
    • 34
    • 35
    • 36
    • 37
    • 38

    这里 ,首先定义了一个类 Customer 和 两个函数对象 CustomerEqCustomerHash。然后 定义 Overloader,继承于可变参数的基类。通过如下申明,CustomerOP 可以使能两个基类的 operator() 的实现。

    using CustomerOP = Overloader<CustomerHash,CustomerEq>;
    
    • 1

    后续章节将继续讨论该技术的应用。

    参考

    • http://www.tmplbook.com
  • 相关阅读:
    HTML中script 标签中的那些属性
    Chapter 8 Intermediate Shell Tools II
    Web后端开发_01
    ubuntu20.04 创建ros环境、创建rospackage
    leetcode做题笔记179. 最大数
    MFC Windows 程序设计[152]之耍酷滑动滚动条
    在Windows环境下将Tomcat发布成服务,并配置JVM参数
    S/4 HANA 大白话 - 财务会计-1
    每日刷题记录 (二十五)
    Java拷贝之深拷贝与浅拷贝
  • 原文地址:https://blog.csdn.net/Dong_HFUT/article/details/125473807