zoukankan      html  css  js  c++  java
  • c++11-17 模板核心知识(四)—— 可变参数模板 Variadic Template

    模板参数接收任意数量的参数。

    定义与使用

    定义:

    void print() {}
    
    template <typename T, typename... Types> 
    void print(T firstArg, Types... args) {
      std::cout << firstArg << '
    '; // print first argument
      print(args...);                // call print() for remaining arguments
    }
    

    使用:

    std::string s("world");
    print (7.5, "hello", s);
    

    C和GO都有类似的概念和定义方式,很好理解。定义void print() {}是为了终止递归。

    args被叫做function parameter pack.

    sizeof...

    返回parameter pack个数:

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

    也许有人会想利用sizeof...来判断:只有当可变参数模板的参数个数大于0时,才调用print,这样可以省略void print() {}

    template <typename T, typename... Types> 
    void print(T firstArg, Types... args) {
      std::cout << firstArg << '
    ';
      if (sizeof...(args) > 0) { // error if sizeof...(args)==0
        print(args...);          // and no print() for no arguments declared
      }
    }
    

    但是这样是错误的,因为模板在编译阶段也会将if的所有代码都进行编译,而不会去根据if的条件去进行选择性的编译,选择运行if的哪个分支是在运行期间做的。

    Compile-Time If

    但是c++17引入了编译期的if(Compile-Time If),所以上面的代码可以这么写:

    template <typename T, typename... Types>
    void print(T const &firstArg, Types const &... args) {
      std::cout << firstArg << '
    ';
      if constexpr (sizeof...(args) > 0) {
        print(args...); // code only available if sizeof...(args)>0 (since C++17)
      }
    }
    

    if constexpr是c++17中编译期if的语法。这样就可以进行在编译期决定编译if条件的哪个分支。再举个例子:

    template <typename T>
    std::string asString(T x)
    {
        if constexpr(std::is_same_v<T, std::string>) {
            return x;   //如果T不是string就是无效的语句
        }
        else if constexpr(std::is_arithmetic_v<T>) {
            return std::to_string(x);   //如果x不是数字就是无效的语句
        }
        else {
            return std::string(x);  //如果不能转换为string就是无效的语句。
        }
    }
    

    折叠表达式 Fold Expressions

    从c++17开始,折叠表达式可以将二元运算符作用于所有parameter pack的参数上:

    Fold Expression Evaluation
    ( ... 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 )))

    比如求parameter pack的和:

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

    再比如上面的print例子可以简写成:

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

    如果想要在每个参数中间输出空格,可以配合lambda:

    template <typename FirstType, typename... Args>
    void print(FirstType first, Args... args) {
      std::cout << first;
    
      auto printWhiteSpace = [](const auto arg) { std::cout << " " << arg; };
    
      (..., printWhiteSpace(args));
    }
    
    int main() { 
      print("hello","world","zhangyachen"); 
    }
    

    其中, (..., printWhiteSpace(args));会被展开为:printWhiteSpace(arg1), printWhiteSpace(arg2), printWhiteSpace(arg3)这样的格式。

    其他场景

    Variadic Expressions

    比如将每个parameter pack的参数double:

    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
    }
    

    还可以用在Compile-time Expression中,比如下面的函数会判断所有的参数类型是否一致:

    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
    

    Variadic Indices

    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]);
    

    Variadic Class Templates

    比如标准库的Tuple:

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

    Variadic Deduction Guides

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

    关键点:

    • enable_if_t控制是否启用该模板。 这个后面文章会讲到。
    • is_same_v<T, U> && ...判断数组元素类型是否相同,跟上面提到的例子用法一样。

    Variadic Base Classes and using

    c++17的新特性,中文翻译应该叫:变长的using声明。C++17尝鲜:变长 using 声明这篇文章关于using的来龙去脉讲的很清楚,推荐大家看看。

    一个更实际的例子:

    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;
      ...
    }
    

    这里给unordered_set提供自定义的HashKeyEqual

    关于可变参数模板的应用场景和各种使用技巧有很多,这里只列了5种大方向的应用场景,但是起码下次遇到看不懂的地方时,知道往哪个大方向去查,不至于一头雾水 :)

    (完)

    朋友们可以关注下我的公众号,获得最及时的更新:

  • 相关阅读:
    Python并发编程之多进程(实战)
    ThreadPoolExecutor源码分析
    JDK 1.8 JVM的变化
    JDK1.8 hashMap源码分析
    Spring解决循环依赖
    spring
    实现一个可重入锁和不可重入锁
    B树与B+树
    WebMagic
    Java高频面试题
  • 原文地址:https://www.cnblogs.com/zhangyachen/p/13946450.html
Copyright © 2011-2022 走看看