闭关之 C++ Template 笔记(一):PartⅠ基本概念(一)

前言

   并发编程和函数式编程的书已经看完了。感觉就是,并发编程推荐使用函数式编程和 CSP,所以看了函数式编程相关的书籍;而 C++ 函数式编程又不得不依赖于模板和模板元编程。因此,买了一本《C++ Template 第二版》。已经开发这么多年了,模板已经不陌生了,但还是要再看一下,查缺补漏,温故知新。继续加油吧!

  • 由于只有英文版,所以笔记可能有会有错误。毕竟英语是硬伤。
    • 本来想买台版的,上次买了一本台版《Animation》就花了两个月。这次还是自己翻译吧!
  • 官方资源
    • 可以去卖书的平台下载
  • 官方代码笔记放到这里: https://gitee.com/thebigapple/Study_CPlusPlus_20_For_CG.git

第一章 Function Templates

1.1 初识函数模板

1.1.1 定义模板

  • 模板定义的语法
    • template< comma-separeted-list-of-parameters>
    • 除了可以使用 typename 之外,还可以使用 class 定义类型参数
      • 建议优先使用 typename
      • 不可以使用 struct

1.1.2 使用模板

  • 作用域限定符 ::
    • 程序将会优先在全局作用域中查找函数模板
      • ::max(f1, f2);
  • 在编译阶段,模板会为程序每一个使用该模板的类型产生一个独立的函数实体
    • 这个过程叫做模板实例化
    • 模板实例化不需要手动请求,只需要使用函数模板就会触发实例化过程
  • void 也可以作为模板参数

1.1.3 二阶段翻译

  • 在实例化模板的时候,如果模板的参数类型不支持使用到的操作符,将会遇到编译器错误
    • 因为模板会被分两个步骤进行编译
      • 模板定义阶段(未实例化),模板的检查包含以下几个方面
        • 语法检查,例如缺少分号
        • 使用了未定义的不依赖于模板参数的名称
        • 未使用模板参数进行检查的静态断言( static assertions)
      • 模板实例化阶段,为了确保所有代码的有效性,模板会再次被检查
        • 所有依赖于模板参数的部分都会进行 double-checked
        template<typename T>
        void foo(T t)
        {
          //如果 undeclared()未定义,第一阶段报错
          undeclared(); 
          //如果 undeclared(t)未定义,第二阶段报错
          undeclared(t);
          static_assert(sizeof(int) > 10,"int too small"); 
          static_assert(sizeof(T) > 10, "T too small"); 
        }
        
    • 实际上,名称被检查两次称为 “two-phase lookup”
      • 详情 14.3.1,Page 249
  • 有些编译器不会在第一阶段执行所有的检查。因此,如果模板没有被实例化过的话,可能不会发现模板代码中的问题
  • 编译和连接
    • 二阶段翻译的一个严重的问题
      • 当实例化一个模板时,编译器需要知道模板的完整定义
      • 第9章会讨论如何应对
      • 暂时的做法:将模板的实现写在头文件里

1.2 模板参数推断

  • 类型推断中的类型转换
    • 类型推断过程中类型转换是受限制的
      • 当调用参数为引用时,禁止任何形式的类型转换。通过相同模板类型参数 T 推断出的两个参数,必须严格匹配(两个参数类型必须一致)
      • 当调用参数为值传递时,只支持 decay 转换
        • const 和 volatile 会被忽略
        • 引用被转换为被引用类型
          • 这里所说的引用应该是常引用 (int const&)
        • raw array 和 function 被转换成对应的指针类型
        • 通过相同模板类型参数 T 推断出的两个参数,其退化(decay)后类型必须一致
  • 模板参数推断错误的几种解决办法 (这部分作为了解,实际开发中有很多制约手段)
    • 强制类型转换
    • 显式的提供参数类型,避免编译器自动推断
    • 使用不同模板的参数
  • 对默认形参的类型推断
    • 类型推断不适用任何默认形参
    • 如果要支持默认形参,需要为模板参数声明一个默认类型参数
      template<typename T = std::string>
      void f(T = "");
      

1.3 多模板参数

  • 当模板接受两个不同类型的模板参数,如果用其中一个模板参数类型作为返回值类型,这会导致一个问题
    • 重复调用该函数时,由于函数实参类型的变化,返回值类型也会发生变化
  • C++ 提供了不同方式应对这一问题
    • 引入第三个模板参数作为返回值类型
    • 让编译器推断返回值类型
    • 将返回值类型定义为两个推断出参数类型的 “公共类型”

1.3.1 返回类型的模板参数

  • 方法一
    • 引入第三个模板参数作为返回值类型
    • 然后,显式的指定三个模板参数的类型
    • 问题
      • 写法比较啰嗦
  • 方法二
    • 将返回值类型模板参数调整到模板定义第一位
    • 然后,显式指定返回值模板参数类型,其它自动推断
    • 问题
      • 虽然只用显式指定一个模板参数类型,但还是需要显示指定
  • 建议
    • 使用单模板参数是比较好的选择

1.3.2 推断返回类型

  • C++14 开始,如果返回值类型是由模板参数决定的,建议让编译器推断返回值类型
    template<typename T1, typename T2>
    auto max (T1 a, T2 b)
    {
      return b < a ? a : b;
    }
    
  • C++14 以前需要使用尾置返回类型
    template<typename T1, typename T2>
    auto max (T1 a, T2 b) -> decltype(b<a?a:b)
    {
      return b < a ? a : b;
    }
    
    • decltype 会根据表达式结果确定返回值类型,这里可以简化,使用 true 作为条件
      template<typename T1, typename T2>
      auto max (T1 a, T2 b) -> decltype(true?a:b)
      {
        return b < a ? a : b;
      }
      
    • 如果 decltype 中的表达式返回值类型是引用类型,可以使用 decay
      #include <type_traits>
      
      template<typename T1, typename T2>
      auto max (T1 a, T2 b) -> typename std::decay<decltype(true? a:b)>::type
      {
        return b < a ? a : b;
      }
      

1.3.3 作为通用类型返回

  • C++11 开始,标准库提供 std::common_type<>::type
    #include <type_traits>
    
    template<typename T1, typename T2>
    std::common_type_t<T1,T2> max (T1 a, T2 b)
    {
      return b < a ? a : b;
    }
    
  • C++11
    • typename std::common_type<T1,T2>::type
  • C++14
    • std::common_type_t<T1,T2>

1.4 默认模板参数

#include <type_traits>

template<typename T1,
         typename T2 = long, 
         typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
  return b < a ? a : b;
}
  • std::decay_t<> 可以确保返回的值不是引用类型。
  • T1() T2() 要求两个模板参数必须有默认构造函数
    • 如果没有可以使用 std::declval
  • 也可以使用 std::common_type<> 作为返回值类型的默认值
    #include <type_traits>
    
    template<typename T1, 
            typename T2, 
            typename RT = std::common_type_t<T1,T2>>
    RT max (T1 a, T2 b)
    {
      return b < a ? a : b;
    }
    
    • std::common_type<> 也会做类型退化,因此返回值类型不会是引用

1.5 重载函数模板

  • 非模板函数,可以和同名的函数模板共存,而且该同名的函数模板可以实例化出与非模板函数有相同形参和返回值类型。
    • 调用原则
      • 模板解析会优先选用非模板函数,而不是将模板函数实例化
      • 如果模板可以实例化一个更匹配的函数,优选选择该模板
      • 如果要强制使用模板实例化的函数进行调用,可以显示指定一个空模板列表
        • ::max<>(7, 42);
      • 推断模板参数不涉及自动类型转换,而常规函数实参可以根据形参类型自动转换
  • 关于字符串类型的模板参数推导可以详看Page 16

1.6 但是,难道我们不应该…?

1.6.1 按值传递还是按引用传递?

  • 建议
    • 除了简单类型 (如:基础类型和 std::string_view) 外,其他建议按照引用传递
    • 这样可以避免不必要的拷贝
  • 按值传递的优势
    • 语法简单
    • 编译器能更好的进行优化
    • 移动语义会使拷贝成本较低
    • 某些情况下可能没有拷贝或移动
  • 对使用模板的建议
    • 模板形参即可能是简单类型,也可能是复杂类型,如果使用按引用传递,对简单类型不利
    • 可以使用 std::ref()std::cref() 强制按引用传递参数
    • 虽然按值传递字符串字面量 和 raw array 会遇到问题,但按引用传递,问题更严重
      • 后期会讨论

1.6.2 为什么不用 inline 呢?

  • 建议
    • 函数模板不需要声明成 inline
    • 例外
      • 全特化模板时,由于其已不是“泛型”。因此,根据情况可以声明为 inline
  • inline 只表明在程序中,函数的定义可能在程序中多次出现
    • inline 会给编译器建议
      • 在调用该函数的地方该函数应该被展开
        • 在某些情况下可以提高效率
        • 但也可能降低效率
  • 现代编译器在没有 inline 关键字的情况下,也可以很好的决定是否将函数展开
  • 但编译器依然会将 inline 纳入是否展开函数的考虑因素中

1.6.3 为什么不用 constexpr 呢?

  • C++11 支持 constexpr 在编译期进行求值
    template<typename T1, typename T2>
    constexpr auto max (T1 a, T2 b)
    {
      return b < a ? a : b;
    }
    ...
    int a[::max(sizeof(char), 1000u)];
    

第二章 Class Template

  • Code_2_1_1

2.1 类模板 Stack 的实现

2.1.1 类模板声明

  • 声明
    template<typename T>
    class Stack {...};
    
  • 用关键字 class 声明
    template<class T>
    class Stack {...};
    
  • 在类模板内部,T 可以像普通类型一样,用于声明成员变量和成员函数
  • 除了在该模板类内部使用外,其他位置使用类模板定义类型都要使用 Stack<T>
  • 构造函数示例
    • 在类内部定义的两种方式
      class Stack 
      {Stack (Stack const&); // copy constructor
        Stack& operator= (Stack const&); // assignment operator};
      
      class Stack 
      {Stack (Stack<T> const&); // copy constructor
        Stack<T>& operator= (Stack<T> const&); // assignment operator};
      
    • 一般使用 为了特殊处理,所以建议使用第一种方式定义
  • 在类外使用
    template<typename T>
    bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);
    
  • 与非模板类不同,不能在函数内部或块作用域内定义类模板
  • 只能在全局作用域、命名空间、其他类的内部定义类模板

2.1.2 成员函数的实现

  • 类内部实现
    template<typename T>
    class Stack {
      void push (T const& elem) 
      {
        elems.push_back(elem); // append copy of passed elem
      }
    };
    
  • 类外部实现
    template<typename T>
    void Stack<T>::push (T const& elem)
    {
      elems.push_back(elem); // append copy of passed elem
    }
    

2.2 类模板 Stack 的使用

  • 在 C++17 以前需要显式指定模板参数类型
    • Stack< int> intStack;
  • C++17 如果能从构造函数推断类型,无须指定
    • Stack intStack{2};
  • 只有在模板函数和模板成员函数被调用时才会被实例化
  • 如果类模板有静态成员,每个使用该类模板的类型都会实例化其相应的静态成员一次
  • 类模板的模板参数可以是任意类型
    • 唯一要求
      • 使用的类型必须支持该模板类中对模板参数的各种操作

2.3 类模板的局部使用

  • 在类模板中,模板参数不必作用所有成员函数或成员变量
  • 但会出现一个问题
    • 如果类模板的非模板成员函数使用模板参数时,遇到实例化参数不支持的操作时,会导致错误
      • 该错误只能在调用该成员函数时被发现

2.3.1 Concepts

  • Concepts
    • 表示一组反复被模板库要求的限制条件
    • 如标准库的 random access iterator 和 default constructible
  • C++11 开始可以使用 static_assert 和一些预定义 type traits 做一些简单的检查
  • C++20 可以使用 Concepts 做复杂情况的检查

2.4 友元

  • 这里使用一个 operator<< 的非成员函数实现介绍友元
  • 需求
    • 比通过 printOn() 来打印 stack 的内容更好的办法是重载 stackoperator<<
    • 使用非成员函数的 operator<< ,并在实现中调用 printOn()
      • 非模板函数
    friend std::ostream& operator<< (std::ostream& strm, Stack<T>const& s) 
    {
      s.printOn(strm);
      return strm;
    }
    
  • 如果要先声明友元函数,再定义就比较复杂
    • 有两种解决方式
      • 将该友元函数声明为一个函数模板, 再定义
      template<typename T>
      class Stack 
      {
        template<typename U>
        friend std::ostream& operator<< (std::ostream&, Stack<U> const&);
      };
      
      • 先前置声明一个输出 Stack<T>operator<< 模板, 再声明友元函数
      //声明 Stack 的 operator<< 前先声明 Stack<T>
      template<typename T>
      class Stack;
      // Stack 的 operator<< 
      template<typename T>
      std::ostream& operator<< (std::ostream&, Stack<T> const&);
      template<typename T>
      class Stack 
      {
        // 友元 operator<< 的声明
        // 注意:
        //      operator<< <T> 相当于声明了一个特化之后的非成员函数模板
        //      如果没有 <T> 代表定义了一个新的非模板函数
        friend std::ostream& operator<< <T> (std::ostream&, Stack<T> const&);
      };
      

2.5 类模板特化

  • 与函数模板特化类似
  • 注意
    • 特化类模板时,建议特化所有成员函数
    • 被特化的类模板
      • 将类模板参数替换成特化的类型即可

2.6 偏特化

  • 类模板可以被偏特化。从而为某些特殊情况提供特殊的实现
  • 如偏特化一个 Stack<> 专门处理指针
    template<typename T>
    class Stack<T*> 
    {
    private:
      std::vector<T*> elems; 
    public:
      void push(T*); 
      T* pop(); 
      T* top() const; 
      bool empty() const ;
    };
    
  • 多模板参数的偏特化
    // partial specialization: both template parameters have same type
    template<typename T>
    class MyClass<T, T> {};
    // partial specialization: second type is int
    template<typename T>
    class MyClass<T, int> {};
    // partial specialization: both template parameters are pointer typestemplate<typename T1, typename T2>
    class MyClass<T1*, T2*> {};
    

2.7 默认类模板参数

  • 与函数模板类似
    template<typename T, typename Cont = std::vector<T>>
    class Stack {...};
    

2.8 类型别名

  • Typedefs and Alias Declarations
    • 有两种方法定义别名
      • 使用关键字 typedef
        • typedef Stack<int> IntStack;
      • 使用关键字 using (C++11)
        • using IntStack = Stack<int>;
    • 建议使用 using
    • 定义新名字的方式称为 type alias declaration。
    • 新名字称为 type alias
  • Alias Templates
    • C++11 开始支持别名模板
      template<typename T>
      using DequeStack = Stack<T, std::deque<T>>;
      
  • Alias Templates for Member Types
    • 为类模板的成员类型定义一种快捷方式
    • 有如下定义
      template<typename T>
      struct MyType 
      {
        typedef … iterator;};
      
      template<typename T>
      struct MyType
      {
        using iterator =;};
      
    • 可以进行如下定义
      template<typename T>
      using MyTypeIterator = typename MyType<T>::iterator;
      
    • 可以使用
      • MyTypeIterator< int> pos;
    • 代替
      • typename MyType<T>::iterator pos;
  • Type Traits Suffix_t
    • C++14 标准库所有返回一个类型的type trait 定义了快捷方式
      • C++11
        • typename std::add_const<T>::type
      • C++14
        • std::add_const_t<T>

2.9 类模板参数推断

  • C++17 以前必须显式指定模板参数类型
  • C++17 如果能从构造函数推断类型,无须指定
    template<typename T>
    class Stack {
    private:
      std::vector<T> elems; // elements
    public:
      Stack () = default;
      Stack (T const& elem) // initialize stack with one element
        : elems({elem}) 
      {
      }};
    
  • 声明示例
    • C++17 以前
      • Stack<int> intStack;
    • C++17
      • Stack intStack = 0;
  • 字符串字面量的类模板参数推断
    • C++17
      • Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17
    • 问题
      • 当参数是按照 T 的引用传递时,参数类型不会被退化,上述形式被转化为
        • Stack< char const[7]>
        • 这样就不能添加不同维度的字符串字面量了
      • 当按值传递时,参数类型会被退化成 char const *
    • 改进
      • 构造函数改成按值传递参数
  • Deduction Guides
    • 上面的例子虽然按值传递,但其为指针,为了避免处理指针可以使用 deduction guides
    • Stack( char const*) -> Stack<std::string>;
    • 指引语句必须出现在该类模板定义相同的作用域或命名空间内。
    • 通常它紧跟着类模板的定义
    • -> 后面的类型被称为推断指引的 “guided type”

2.10 模板聚合

  • 聚合类
    • class 或者 struct
      • 没有用户定义的显式的,或继承的构造函数
      • 没有 private 或者 protected 的非静态成员,
      • 没有虚函数
      • 没有 virtual,private 或者protected的基类
    • 也可以是模板
  • 示例
    template<typename T>
    struct ValueWithComment {
      T value;
      std::string comment;
    };
    
  • 使用
    ValueWithComment< int> vc;
    vc.value = 42;
    vc.comment = "initial value";
    
  • C++17 聚合类的类模板可以使用 deduction guides
    ValueWithComment(char const*, char const*) -> ValueWithComment<std::string>;
    ValueWithComment vc2 = {"hello", "initial value"};
    
  • std::array<> 是一个聚合类

第三章 非类型模板参数

3.1 非类型类模板参数

  • 定义固定大小容器
    • 优势
      • 可以避免开发人员或标准库管理内存
    • 缺点
      • 具体大小很难确定
    • 改进
      • 让用户根据各自的情况指定容器大小
      • 类模板使用非类型参数
  • 非类型参数
    • 可以为常规数值
  • 非类型的模板参数
    • 不再是类型,而是某个值
  • Code_3_1_1
  • 非类型的模板参数示例
    • 定义
      template<typename T, std::size_t Maxsize>
      class Stack {
      private:
        std::array<T,Maxsize> elems; 
      };
      
    • 使用
      • Stack<int,20> int20Stack;
  • 可以指定默认值
    template<typename T = int, std::size_t Maxsize = 100>
    class Stack {};
    
    • 不建议这样做
      • 最好同时显式地指定两个模板参数

3.2 非类型函数模板参数

  • 与非类型类模板参数定义类似
    template<int Val, typename T>
    T addValue (T x)
    {
      return x + Val;
    }
    
  • 根据非类型模板参数推断返回值类型
    template<auto Val, typename T = decltype(Val)>
    T foo();
    
  • 保证传入的非类型模板参数的类型与模板参数类型一致
    template<typename T, T Val = T{}>
    T bar();
    

3.3 非类型模板参数的限制

  • 可以是
    • 整型(包括枚举类型)
    • 指向 objects/functions/members 的指针
    • objects 或者 functions 的左值引用
    • std::nullptr_t
  • 不可以是
    • 浮点型数值
  • 当传递对象的指针或者引用作为模板参数时,对象不能是
    • 字符串字面量
    • 临时变量
    • 数据成员
    • 其它子对象
  • 针对 C++ 不同版本的限制
    • C++11,对象必须要有外部链接。
    • C++14,对象必须是外部链接或者内部链接
    • C++17,对象没有链接属性也是有效的
  • 避免无效表达式
    • 非类型模板参数可以是任何编译期表达式
      • C<sizeof(int) + 4, sizeof(int) == 4> c;
    • 如果表达式中使用了 operator > 注意其与 <> 冲突
      • 错误
        • C<42, sizeof(int) > 4> c;
      • 改正
        • C<42, (sizeof(int) > 4)> c;

3.4 模板参数 auto

  • C++17 可以不指定非类型模板参数的具体类型
    • 使用 auto
    • template<typename T, auto Maxsize>
  • 如果要确定 auto 的类型可以使用
    • using size_type = decltype(Maxsize);
  • C++14 也可以使用 auto,让编译器推断出具体的返回值类型
    auto size() const 
    { 
      return numElems;
    }
    
  • 将字符串作为常量数组用于非类型模板参数有如下用法
    #include <iostream>
    
    template<auto T> // take value of any possible nontype parameter (since C++17)
    class Message 
    {
    public:
      void print() 
      {
        std::cout << T << '\n';
      }
    };
    
    ...
    
    Message<42> msg1;
    msg1.print(); // initialize with int 42 and print that value
    static char const s[] = "hello";
    Message<s> msg2; // initialize with char const[6] "hello"
    msg2.print(); // and print that value
    

第四章 变参模板

4.1 变参模板

4.1.1 变参模板实例

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

4.1.2 重载变参和非变参模板

#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 remainingarguments
}
  • 调用 print() 的重载时会优先选择没有参数包的函数模板

4.1.3 运算符 sizeof

  • C++11 引入 sizeof...
    • 一个参数包包含元素的数量
      template<typename T, typename… Types>
      void print (T firstArg, Types… args)
      {
        std::cout << firstArg << '\n'; //print first argument
        std::cout << sizeof(Types) << '\n'; //print number of remainingtypes
        std::cout << sizeof(args) << '\n'; //print number of remainingargs}
      

4.2 折叠表达式

  • C++17 提供了一个特性
    • 使用 binary operator 计算所有参数包(可选初始值)参数的运算结果
    • 如:计算参数包所有参数的和
      template<typename … T>
      auto foldSum (T… s) 
      {
        return (+ s); // ((s1 + s2) + s3) …
      }
      
      • 如果参数包为空,上述表达式不合法
  • C++17 可以折叠的表达式
    • ( … 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 )))
  • 几乎所有的 binary operator 都可以用于折叠表达式
    template<typename T, typename… TP>
    Node* traverse (T np, TP… paths) 
    {
      return (np ->*->* paths); // np ->* paths1 ->* paths2 …
    }
    

4.3 变参模板的应用

  • 应用之一就是转发任意类型和数量的参数
    • 通常使用移动语义对参数进行完美转发(perfectly forwarded)
      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);};
      }
      
    • 常规模板参数的规则同样适用于变参模板参数
      • 参数是按值传递,其参数会被拷贝,类型也会退化
        • template<typename… Args> void foo (Args… args);
      • 参数是按引用传递,其参数会是实参的引用,且类型不会退化
        • template<typename… Args> void bar (Args const&… args);

4.4 变参类模板和变参表达式

  • 参数包还可以应用于其他地方
    • 表达式
    • 类模板
    • using 声明
    • deduction guides

4.4.1 变参表达式

  • 输出参数包中的每个参数的倍数
    template<typename… T>
    void printDoubled (T const&… args)
    {
      print (args + args…);
    }
    
  • 参数包中的每个参数加 1
    template<typename… T>
    void addOne (T const&… args)
    {
      print (args + 1); // ERROR: 1… is a literal with too many decimalpoints
      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
    }
    

4.4.2 变参下标 (Variadic Indices)

  • 函数通过变参下标(参数包)访问某参数的相应元素
    template<typename C, typename… Idx>
    void printElems (C const& coll, Idx… idx)
    {
      print (coll[idx]);
    }
    
    • 调用
      • 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]);
    }
    
    • 调用
      • printIdx<2,0,3>(coll);

4.4.3 变参类模板

  • Tuple 的实现
    template<typename… Elements>
    class Tuple;
    
    Tuple<int, std::string, char> t; // t can hold integer, string, andcharacter
    
  • Variant 的实现
    template<typename… Types>
    class Variant;
    
    Variant<int, std::string, char> v; // v can hold integer, string,orcharacter
    
  • 定义一个表示一组下表的类
    // type for arbitrary number of indices:
    template<std::size_t…>
    struct Indices {};
    
    • 示例:打印 std::array 或者 std::tuple 中元素的函数
      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>());
      
    • 使用
      auto t = std::make_tuple(12, "monkeys", 2.0);
      printByIdx(t, Indices<0, 1, 2>());
      

4.4.4 变参推断指南 Deductions Guides

  • std::array 的 Deductions Guide
    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
    • std::array a{42,45,77};
  • enable_if_t()
    • 如果是 false 类型推断失败
    • 这种方式可以确保所有元素是同一类型

4.4.5 变参基类和 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; }
    };
    
    //比较 Customer 对象
    struct CustomerEq
    {
      bool operator() (Customer const& c1, Customer const& c2) const
      {
        return c1.getName() == c2.getName();
      }
    };
    
    //计算对象hash值
    struct CustomerHash 
    {
      std::size_t operator() (Customer const& c) const
      {
        return std::hash<std::string>()(c.getName());
      }
    };
    
    //从个数不定的基类派生出了一个新的类
    //绑定每个基类中 operator() 的声明
    // define class that combines operator() for variadic baseclasses:
    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;}
    

第五章 基础技巧

5.1 关键字 typename

  • typename
    • 阐明模板中的一个标识符代表的是某种类型
      template<typename T>
      class MyClass 
      {
      public:void foo() 
        {
          typename T::SubType* ptr;
        }
      };
      
    • 第二个 typename 被用来阐明 SubType 是定义在 T 中的一个类型
      • ptr 是一个指向 T::SubType 类型的指针
    • 如果没有 typename 的话,SubType 会被假设成一个非类型成员
      • 如静态成员或枚举
  • 当一个依赖于模板参数的名称代表的是某种类型的时候,就必须使用 typename

5.2 零初始化

  • 对于基础类型,如 int,double 及指针类型,由于它们没有默认构造函数,因此它们不会被默认初始化,需要使用 0 显式初始化
  • 在定义模板时,如果想让一个模板类型的变量被初始化成一个默认值,需要考虑无默认构造的数据类型
    • 使用 value initialization 方法
      • T x{};
    • C++11 之前
      • T x = T();
      • C++17 之前,只有在与拷贝初始化对应的构造函数没有被声明为 explicit 的时候,这一方式才有效
    • C++17 开始,由于强制拷贝省略(mandatory copy elision)的使用,这一限制被解除,因此以上两种方式都有效。
  • 花括号初始化
    • 如果没有可用的默认构造函数,还可以使用 initializer-list constructor
    • T x{1, 2};
  • 默认的构造函数对相应成员做初始化
    • C++11 之后
      • MyClass() : x{} {}
    • C++11 之前
      • MyClass() : x() {}
  • C++11 之后可以通过如下方式对非静态成员进行默认初始化
    • T x{};
    • 不能对函数的默认参数使用这一方式
      • void foo(T p{}) {} // error
    • 改用
      • void foo(T p = T{})

5.3 this->的使用

  • 当使用定义在基类中的、依赖于模板参数的成员时,用 this-> Base<T>:: 来修饰调用函数
    template<typename T>
    class Base 
    {
    public:
      void bar();
    };
    
    template<typename T>
    class Derived : Base<T> 
    {
    public:
      void foo() {
        bar(); // calls external bar() or error
        Base<T>::bar();
      }
    };
    

5.4 原始数组和字符串字面量模板

  • 当向模板传递原始数组和或者字面量时需注意
    • 如果参数是按引用传递的,那么参数类型不会退化
      • "hello" 会被推断为 char const[6]
    • 如果参数是按值传递的,那么参数类型会退化
      • 字符串字面量会被退化成 char const *
  • 处理原始数组或字符串字面量的模板示例
    template<typename T, int N, int M>
    bool less (T(&a)[N], T(&b)[M])
    {
      for (int i = 0; i<N && i<M; ++i)
      {
        if (a[i]<b[i]) return true; 
        if (b[i]<a[i]) return false;
      }
      return N < M;
    }
    ...
    
    int x[] = {1, 2, 3};
    int y[] = {1, 2, 3, 4, 5};
    //T会被实例化成 int,N 被实例化成 3,M 被实例化成 5
    std::cout << less(x,y) << '\n';
    std::cout << less("ab","abc") << '\n';
    
  • 用来处理字符串字面量的函数模板示例
    template<int N, int M>
    bool less (char const(&a)[N], char const(&b)[M])
    {
      for (int i = 0; i<N && i<M; ++i) 
      {
        if (a[i]<b[i]) return true;
        if (b[i]<a[i]) return false;
      }
      return N < M;
    }
    
  • 对处理数组的函数模板的所有可能的重载示例
    • Code_5_4_1
    • 对特殊情况做的特化
      • 边界已知和未知的数组
      • 边界已知和未知的数组的引用
      • 指针
    • 针对未知边界数组定义的模板,可以用于不完整类型
      • extern int i[];
      • 当按照引用传递时,它的类型是 int(&)[]

5.5 成员模板

  • 类的成员也可以是模板
    • 如嵌套类和成员函数
  • 设有一个 stack 模板,通常只有当两个实例化的模板类型相同时才可以相互赋值
    • 如果两个实例化的参数类型可以隐式转换也不能相互赋值
    Stack<int> intStack1, intStack2; // stacks for ints
    Stack<float> floatStack; // stack for floats
    intStack1 = intStack2; // OK: stacks have same type
    floatStack = intStack1; // ERROR: stacks have different types
    
  • 为了实现可以对模板实例化的不同参数类型实例相互赋值,可以将赋值运算符定义成模板
    • Code_5_5_1
    • 在模板类型为 T 的模板内部,定义一个模板类型为T2 的内部模板语法
      template<typename T>
      template<typename T2>
    • 为了访问 op2 的成员,可以将其它所有类型的 stack 模板的实例都定义成友元
      • template<typename> friend class Stack;
  • Code_5_5_1 如果将 string 类型的 stack 赋值给 int 类型的 stack 会报错
    • 需要改变内部的容器类型
      • Code_5_5_2 解决上述问题
      • 其实就是将容器类型也设置为类模板的模板参数
  • 特化成员模板
    • 成员函数模板也可以被全部或者部分地特化
      class BoolString 
      {
      private:
        std::string value;
      public:
        BoolString (std::string const& s)
          : value(s) {}
          
        template<typename T = std::string>
        T get() const // get value (converted to T)
        { 
          return value;
        }
      };
      
    • 成员函数模板进行全特化
      //为避免重复定义的错误,必须将它定义成 inline 的。
      template<>
      inline bool BoolString::get<bool>() const 
      {
        return value == "true" || value == "1" || value == "on";
      }
      
    • 注意我们不需要也不能够对特化模板进行声明
    • 使用
      std::cout << std::boolalpha;
      BoolString s1("hello");
      std::cout << s1.get() << ’\n’; //prints hello
      std::cout << s1.get<bool>() << ’\n’; //prints false
      BoolString s2("on");
      std::cout << s2.get<bool>() << ’\n’; //prints true
      
  • 特殊成员函数的模板
    • 当特殊成员函数允许复制和移动对象,也可使用模板
      • 与其他成员模板定义类似
      • 构造函数也可以使用模板
      • 注意
        • 构造函数模板或者赋值运算符模板不会取代预定义的构造函数和赋值运算符
      • 成员函数模板不会被算作用来复制和移动对象的特殊成员函数
        • 相同类型的 stack 之间相互赋值,调用的依然是默认赋值运算符
    • 优势和缺点
      • 构造函数模板或赋值运算符模板可能比预定义的复制/移动构造函数或者赋值运算符更匹配
      • 想要对复制/移动构造函数进行模板化并不是一件容易的事情
        • 如何限制其存在

5.5.1 构造 .template

  • 有时,在调用成员模板的时候需要显式地指定其模板参数的类型。这时候就需要使用关键字 template 来确保 < 为模板参数列表的开始,而不是一个比较运算符
    template<unsigned long N>
    void printBitset (std::bitset<N> const& bs) 
    {
      std::cout << 
          bs.template to_string<char, std::char_traits<char>, std::allocator<char>>();
    }
    
  • .template 标识符(->template 和 ::template 也类似)只能被用于模板内部
    • 并且它前面的对象依赖于模板参数时使用

5.5.2 泛型 Lambda 与成员模板

  • C++14 引入的泛型 lambdas
    • 是定义成员模板的快捷方式
    • lambda 定义
      [] (auto x, auto y) 
      {
        return x + y;
      }
      
    • 编译器构造
      class SomeCompilerSpecificName 
      {
      public:
        SomeCompilerSpecificName(); // constructor only callable bycompiler
        template<typename T1, typename T2>
        auto operator() (T1 x, T2 y) const 
        {
          return x + y;
        }
      };
      

5.6 变量模板

  • C++14开始,变量也可以被某种类型参数化。称为变量模板
    template<typename T>
    constexpr T pi{3.1415926535897932385};
    
    • 不能定义在函数内部或块作用域内
  • 在使用变量模板的时候,必须指明它的类型
    • std::cout << pi<double> << '\n';
    • std::cout << pi<float> << '\n';
  • 变量模板可以用于不同翻译单元
  • 变量模板可有默认模板类型
    template<typename T = long double>
    constexpr T pi = T{3.1415926535897932385};
    ...
    std::cout << pi<> << '\n';
    
  • 变量模板可以用非类型参数进行参数化,也可以用非类型参数进行变量模板的初始化
    • 示例
      #include <iostream>
      #include <array>
      
      template<int N>
      std::array<int,N> arr{}; // array with N elements, zero-initializedtemplate<auto N>
      
      constexpr decltype(N) dval = N; // type of dval depends on passed value
      
      int main()
      {
        std::cout << dval<'c'> << '\n'; // N has value 'c' of type char
        arr<10>[0] = 42; // sets first element of global arr
        for (std::size_t i=0; i<arr<10>.size(); ++i)  // uses valuessetin arr
        {
          std::cout << arr<10>[i] << '\n';
        }
      }
      
  • 用于数据成员的变量模板
    • 定义表示类模板成员
    • 示例
      template<typename T>
      class MyClass 
      {
      public:
        static constexpr int max = 1000;
      };
      
    • 可以为 MyClass<>的不同特化版本定义不同的值
      template<typename T>
      int myMax = MyClass<T>::max;
      
    • 使用
      • auto i = myMax<std::string>;
    • 而不是
      • auto i = MyClass<std::string>::max;
    • 标准库的类使用
      • 示例
        namespace std {
          template<typename T>
          class numeric_limits 
          {
          public:static constexpr bool is_signed = false;};
        }
        
      • 可以定义
        template<typename T>
        constexpr bool isSigned = std::numeric_limits<T>::is_signed;
        
      • 使用
        • isSigned<char>
      • 代替
        • std::numeric_limits<char>::is_signed
  • Type Traits “_v” 后缀
    • C++17 标准库使用变量模板技术为所有的 type traits 产生一个值定义了简化方式
      • std::is_const_v<T> // since C++17
      • std::is_const<T>::value //since C++11
    • 定义如下
      namespace std {
        template<typename T>
        constexpr bool is_const_v = is_const<T>::value;
      }
      

5.7 模板的模板参数

  • 允许模板参数是一个类模板,会很实用
  • 使用 Code_5_5_2 的 stack 模板声明如下
    • Stack<int, std::vector<int>> vStack;
  • 模板的模板参数,在声明 Stack 类模板的时候就可以只指定容器的类型而不去指定容器中元素的类型
    • Stack<int, std::vector> vStack;s
  • 代码如下
    template<typename T,
             //声明模板参数时可以使用 class 代替 typename
             template<typename Elem> class Cont = std::deque>
    class Stack 
    {
    private:
      Cont<T> elems; // elements
    public:
      void push(T const&); // push element
      void pop(); // pop element
      T const& top() const; // return top element
      bool empty() const { // return whether the stack is emptyreturn elems.empty();
      }};
    
  • 与 Code_5_5_2 的 stack 相比,区别在于第二个模板参数被定义为一个类模板
  • C++11 开始,也可以用别名模板(alias template)取代 Cont
  • C++17 开始,在声明模板的模板参数时才可以用 typename 代替 class
    template<typename T, 
            template<typename Elem> typename Cont = std::deque>
    class Stack //ERROR before C++17 
    {};
    
  • 模板的模板参数中的模板参数没有被用到,作为惯例可以省略它
    template<typename T, 
            template<typename> class Cont = std::deque>
    class Stack 
    {};
    
  • 成员函数更改
    template<typename T, template<typename> class Cont>
    void Stack<T,Cont>::push (T const& elem)
    {
      elems.push_back(elem); // append copy of passed elem
    }
    
  • 模板的模板参数匹配
    • 如果使用上面版本的 Stack,可能会遇到错误
      • 默认的 std::deque 和 模板模板参数 Cont 不匹配
    • 原因
      • 在 C++17 之前,模板模板参数必须和实际参数(std::deque)的模板参数匹配
        • std::deque 有两个参数,第二个是默认参数 allocator
        • 默认参数也要被匹配
    • 修正
      template<typename T,
              template<typename Elem,
                        typename Alloc = std::allocator<Elem>> class Cont = std::deque>
      class Stack 
      {
      private:
        Cont<T> elems; // elements};
      
    • Alloc 可以省略掉
    • C++17 可以不修正
  • 修正后完整的 stack 代码
    • Code_5_7_1
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值