Skip to content

Latest commit

 

History

History
353 lines (265 loc) · 11.9 KB

13.第十三章:模板.md

File metadata and controls

353 lines (265 loc) · 11.9 KB

第十三章:模板

1.函数模板

  1. 使用 template 关键字引入模板: template void fun(T) {...}

    1. 函数模板的声明与定义
    2. typename 关键字可以替换为 class ,含义相同
    3. 函数模板中包含了两对参数:
      1. 函数形参 / 实参
      2. 模板形参 / 实参
    #include <iostream>
    
    // 函数模板的声明, 可以多次声明
    template<typename T>
    void fun(T);
    
    // 函数模板的定义,不能多次定义
    template<typename T> // 模板 <类型模板参数,形式参数T,表面了一种类型>
    void fun(T input) 
    // input 本身是函数模板的形式参数(函数的形式参数);运行期调用
    // <typename T>中的T也是形式参数,称为模板形参
    // 模板形参:需要在编译期赋予相应的实参 -> 将函数模板实例化出相应的函数
    {
        // 函数模板中包含了两对参数:
            // 1. 函数形参 / 实参
            // 2. 模板形参 / 实参
        std::cout << input << std::endl;
    }
    int main()
    {
    }
  2. 函数模板的显式实例化: fun(3)

    1. 实例化会使得编译器产生相应的函数(函数模板并非函数,不能调用)
    2. 编译期的两阶段处理
      1. 模板语法检查
      2. 模板实例化
    3. 模板必须在实例化时可见——翻译单元的一处定义原则
    4. 注意与内联函数的异同
    #include <iostream>
    
    template<class T> // class 也可以,代表 T 是一个类型;但是不能用 struct
    
    // 编译期的两阶段处理:1.模板语法检查
    template<typename T>
    void fun(T input)
    {
        std::cout << input << std::endl;
    }
    int main()
    {
        // int 是模板实参,代替T,实例化出函数void fun<int>(int input) 
        // 3 对应函数实参
        fun<int>(3); // 此时才会 2.模板实例化
        // 实例化会使得编译器产生相应的函数(函数模板并非函数,不能调用)
        fun<double>(3);  // 实例化出两个函数
        
        // 给定模板形参,是显式实例化;还有隐式实例化,更复杂
    }
    ///////C++ insight ///////
    template<typename T>
    void fun(T input)
    {
      (std::cout << input) << std::endl;
    }
    /* First instantiated from: insights.cpp:12 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    void fun<int>(int input)
    {
      std::cout.operator<<(input).operator<<(std::endl);
    }
    #endif
    /* First instantiated from: insights.cpp:14 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    void fun<double>(double input)
    {
      std::cout.operator<<(input).operator<<(std::endl);
    }
    #endif
    ///////////////////////////////////////////////////
    // 1. 模板必须在实例化时可见——翻译单元的一处定义原则
    // 2. 注意与内联函数的异同
    ////////////// header.h //////////////
    #include <iostream>
    
    // 翻译单元的一处定义原则
    template <typename T>
    void fun(T input)
    {
        std::cout << input << std::endl;
    }
    // 目的:在翻译单元可见,从而进行实例化
    
    // inline 相当于把程序级的一处定义原则退化成翻译单元级的一处定义原则
    inline void normal_fun()  
    {
    }
    // inline : 该函数定义的内容在一定情况下需要在调用的地方进行展开,形成内联
    ////////////// header.h //////////////
    template <typename T>
    inline void fun(T input) 
    // 加入inline 是告诉编译器可以选择把这个函数在调用处展开
    // 相应去掉函数调用,实现内联函数相应的功能
    {
        std::cout << input << std::endl;
    }
    //////////////////////////////////////
  3. 函数模板的重载

    #include <iostream>
    
    template <typename T>
    void fun(T input)
    {
        std::cout << input << std::endl;
    }
    
    template <typename T>
    void fun(T* input) // 接收的参数列表的类型不同,也可引入重载
    {
        std::cout << *input << std::endl;
    }
    
    template <typename T, typename T2>
    void fun(T input, T2 input2)
    {
        std::cout << input << std::endl;
        std::cout << input2 << std::endl;
    }
    
    int main()
    {
        int x = 3;
        fun<int>(&x);
    }
  4. 模板实参的类型推导

    1. 如果函数模板在实例化时没有显式指定模板实参,那么系统会尝试进行推导
    2. 推导是基于函数实参(表达式)确定模板实参的过程,其基本原则与 auto 类型推导相似
      1. 函数形参是左值引用 / 指针:
        1. 忽略表达式类型中的引用
        2. 将表达式类型与函数形参模式匹配以确定模板实参
      2. 函数形参是万能引用
        1. 如果实参表达式是右值,那么模板形参被推导为去掉引用的基本类型
        2. 如果实参表达式是左值,那么模板形参被推导为左值引用,触发引用折叠
    3. 函数形参不包含引用
      1. 忽略表达式类型中的引用
      2. 忽略顶层 const
      3. 数组、函数转换成相应的指针类型
  5. 模板实参并非总是能够推导得到

    1. 如果模板形参与函数形参无关,则无法推导
    2. 即使相关,也不一定能进行推导,推导成功也可能存在因歧义而无法使用
  6. 在无法推导时,编译器会选择使用缺省模板实参

    1. 可以为任意位置的模板形参指定缺省模板实参——注意与函数缺省实参的区别
  7. 显式指定部分模板实参

    1. 显式指定的模板实参必须从最左边开始,依次指定
    2. 模板形参的声明顺序会影响调用的灵活性
  8. 函数模板制动推导时会遇到的几种情况

    1. 函数形参无法匹配—— SFINAE (替换失败并非错误)
    2. 模板与非模板同时匹配,匹配等级相同,此时选择非模板的版本
    3. 多个模板同时匹配,此时采用偏序关系确定选择“最特殊”的版本
  9. 函数模板的实例化控制

    1. 显式实例化定义: template void fun(int) / template void fun(int)
    2. 显式实例化声明: extern template void fun(int) / extern template void fun(int)
    3. 注意一处定义原则
    4. 注意实例化过程中的模板形参推导
  10. 函数模板的 ( 完全 ) 特化: template<> void f(int) / template<> void f(int)

    1. 并不引入新的(同名)名称,只是为某个模板针对特定模板实参提供优化算法
    2. 注意与重载的区别
    3. 注意特化过程中的模板形参推导
  11. 避免使用函数模板的特化

    1. 不参与重载解析,会产生反直觉的效果
    2. 通常可以用重载代替
    3. 一些不便于重载的情况:无法建立模板形参与函数形参的关联
      1. 使用 if constexpr 解决
      2. 引入“假”函数形参
      3. 通过类模板特化解决
  12. (C++20) 函数模板的简化形式:使用 auto 定义模板参数类型

    1. 优势:书写简捷
    2. 劣势:在函数内部需要间接获取参数类型信息

2.类模板与成员函数模板

  1. 使用 template 关键字引入模板: template class B {…};

    1. 类模板的声明与定义——翻译单元的一处定义原则
    2. 成员函数只有在调用时才会被实例化
    3. 类内类模板名称的简写
    4. 类模板成员函数的定义(类内、类外)
  2. 成员函数模板

    1. 类的成员函数模板
    2. 类模板的成员函数模板
  3. 友元函数(模板)

    1. 可以声明一个函数模板为某个类(模板)的友元
    2. C++11 支持声明模板参数为友元
  4. 类模板的实例化

    1. 与函数实例化很像
    2. 可以实例化整个类,或者类中的某个成员函数
  5. 类模板的(完全)特化 / 部分特化(偏特化)

    1. 特化版本与基础版本可以拥有完全不同的实现
  6. 类模板的实参推导(从 C++17 开始)

    1. 基于构造函数的实参推导
    2. 用户自定义的推导指引
    3. 注意:引入实参推导并不意味着降低了类型限制!
    4. C++ 17 之前的解决方案:引入辅助模板函数

3.Concepts

  1. 模板的问题:没有对模板参数引入相应的限制

    1. 参数是否可以正常工作,通常需要阅读代码进行理解
    2. 编译报错友好性较差 (vector<int&>)
  2. ( C++20 ) Concepts :编译期谓词,基于给定的输入,返回 true 或 false

    1. 与 constraints ( require 从句)一起使用限制模板参数
    2. 通常置于表示模板形参的尖括号后面进行限制
  3. Concept 的定义与使用

    1. 包含一个模板参数的 Concept
      1. 使用 requires 从句
      2. 直接替换 typename
    2. 包含多个模板参数的 Concept
      1. 用做类型 constraint 时,少传递一个参数,推导出的类型将作为首个参数
  4. requires 表达式

    1. 简单表达式:表明可以接收的操作
    2. 类型表达式:表明是一个有效的类型
    3. 复合表达式:表明操作的有效性,以及操作返回类型的特性
    4. 嵌套表达式:包含其它的限定表达式
  5. 注意区分 requires 从句与 requires 表达式

  6. requires 从句会影响重载解析与特化版本的选取

    1. 只有 requires 从句有效而且返回为 true 时相应的模板才会被考虑
    2. requires 从句所引入的限定具有偏序特性,系统会选择限制最严格的版本
  7. 特化小技巧:在声明中引入“ A||B” 进行限制,之后分别针对 A 与 B 引入特化

4.模板相关内容

  1. 数值模板参数与模板模板参数

    1. 模板可以接收(编译期常量)数值作为模板参数
      1. template class Str;
      2. template <typename T, T value> class Str;
      3. (C++ 17) template class Str;
      4. (C++ 20) 接收字面值类对象与浮点数作为模板参数
        1. 目前 clang 12 不支持接收浮点数作为模板参数
    2. 接收模板作为模板参数
      1. template <template class C> class Str;
      2. (C++17) template <template typename C> class Str;
      3. C++17 开始,模板的模板实参考虑缺省模板实参( clang 12 支持程度有限)
        1. Str 是否支持?
  2. 别名模板与变长模板

    1. 别名模板
      1. 可以使用 using 引入别名模板
        1. 为模板本身引入别名
        2. 为类模板的成员引入别名
        3. 别名模板不支持特化,但可以基于类模板的特化引入别名,以实现类似特化的功能
          1. 注意与实参推导的关系
    2. 变长模板( Variadic Template )
      1. 变长模板参数与参数包
      2. 变长模板参数可以是数值、类型或模板
      3. sizeof... 操作
      4. 注意变长模板参数的位置
  3. 包展开与折叠表达式

    1. 包展开
      1. (C++11) 通过包展开技术操作变长模板参数
      2. 包展开语句可以很复杂,需要明确是哪一部分展开,在哪里展开
    2. 折叠表达式
      1. (C++17) 折叠表达式
        1. 基于逗号的折叠表达式应用
        2. 折叠表达式用于表达式求值,无法处理输入(输出)是类型与模板的情形
  4. 完美转发与 lambda 表达式模板

    1. (C++11) 完美转发: std::forward 函数
      1. 通常与万能引用结合使用
      2. 同时处理传入参数是左值或右值的情形
    2. (C++20) lambda表达式模板
  5. 消除歧义与变量模板

    1. 使用 typename 与 template 消除歧义
      1. 使用 typename 表示一个依赖名称是类型而非静态数据成员
      2. 使用 template 表示一个依赖名称是模板
      3. template 与成员函数模板调用
    2. (C++14) 变量模板
      1. template T pi = (T)3.1415926;
      2. 其它形式的变量模板