第十四章:模版(一)

本文深入探讨了C++中的函数模板,包括模板的引入、类型推导、重载、显式实例化和特化。详细阐述了模板形参与实参的关系,以及在模板实例化过程中可能出现的问题和解决方案。同时,讨论了函数模板的特殊情况,如SFINAE原则和非模板函数的选择优先级。最后,提到了C++20中函数模板的简化形式和避免使用特化的建议。
摘要由CSDN通过智能技术生成

函数模版

我们可以使用template关键字引入模版,比如

template <typename T>
void fun(T input)
{
    
}

typename关键字可以替换为class,含义相同

函数模版中包含了两对参数:函数形参/实参;模版形参/实参

函数模版需要进行显式实例化,比如

fun<int>(3);

关于显式实例化,需要知道以下几点:

  • 实例化会使得编译器产生相应的函数(函数模版并非函数,不能调用)
  • 模版函数在编译期的两阶段处理
    1. 模版语法检查
    2. 模版实例化
  • 模版必须在实例化时可见->模版的翻译单元级的一处定义原则(不同于普通函数的程序级的一处定义原则)
  • 注意函数模版与内联函数的区别

接下来我们讨论一个概念-函数模版的重载

template <typename T>
void fun(T)
{

}

template <typename T, typename T2>
void fun(T input1, T2 input2)
{

}

接下来我们来讨论模版实参的类型推导,主要分为以下几部分

  • 如果函数模版在实例化时没有显式制定模版实参,那么系统会尝试进行推导,推导是基于函数实参(表达式)确定模版实参的过程,其基本原则与auto类型推导类似,主要有以下几条规则:
    1. 函数形参是左值引用/指针:

      • 忽略表达式类型中的引用
      • 将表达式类型与函数形参进行模式匹配以确定模版实参
      template <typename T>
      void fun(T& input){
          std::cout << input << std::endl;
      }
      
      int main() {
          int y = 3;
          int& x = y;
          fun(x);
      }
      
    2. 函数形参是万能引用:

      • 如果实参表达式是右值,那么模版形参被推导为去掉引用的基本类型
      • 若果实参表达式是左值,那么模版形参被推导为左值引用,触发引用折叠
      template <typename T>
      void fun(T&& input){
          std::cout << input << std::endl;
      }
      
      int main() {
          fun(3); // -> int &&
      
          int x = 3;
          fun(x);       // -> int & && -> int &
      }
      
    3. 函数形参不包含引用:

      • 忽略表达式中的引用
      • 忽略顶层const
      • 数组、函数转换成对应的指针类型
      template <typename T>
      void fun(T input){
          std::cout << input << std::endl;
      }
      
      int main() {
          fun(3); // -> int
      
          int x = 3;
          int & ref1 = x;
          fun(ref1);    // -> int
      
          const int & ref2 = x;
          fun(ref2);    // -> int
      
          const int * const ptr = &x;
          fun(ptr);     // -> const int *
      
          int y[3];
          fun(y);       // -> int *
      }
      

关于模版实参的类型推导,我们需要注意以下几点

  • 模版实参并非总能推导得到

    1. 如果模版形参和函数形参无关,则无法推导
      template <typename T,typename Res>
      Res fun(T input){
      
      }
      
      int main() {
          fun(3);
      }
      
    2. 即使相关,也不一定能进行推导,推导成功也可能因存在歧义而无法使用
      template <typename T>
      void fun(unsigned input = sizeof(T)){
          
      }
      
      int main() {
          fun(3);				// 无法推导
      }
      
      template <typename T>
      void fun(T t1, T t2){
      
      }
      
      int main() {
          fun(3,5.0);		// 存在冲突
      }
      
  • 在无法推导时,编译器会选择使用缺省模版实参

    template <typename T = int>
    void fun(unsigned x = sizeof(T)){
    
    }
    
    int main() {
        fun(3);
    }
    

    我们可以为任意位置的模版形参指定缺省模版实参,这里需要注意与函数缺省实参的区别

  • 我们可以显式指定部分模版实参

    1. 显式指定的模版实参必须从最左边开始,依次指定
    2. 模版形参的声明顺序会影响调用的灵活性
      template <typename T, typename Res>
      Res fun1(T input){
      
      }
      
      template <typename Res, typename T>
      Res fun2(T input){
      
      }
      
      int main() {
          fun1<int>(3);       // 无法指定Res
          
          fun2<int>(3); // 可以指定Res
      }
      
  • 函数模版自动推导时候会遇到一些特殊情况

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

之后我们来关注函数模版的实例化控制,其中包括以下几个关键点

  • 显式实例化定义:template void fun<int>(int) / template void fun(int)
  • 显式实例化声明:`extern template void fun(int) / extern template void fun(int)
  • 注意显式实例化定义在程序级别的一处定义原则
  • 注意实例化过程中的模版形参推导

接下来我们来关注与模版实例化非常相似的一个概念,函数模版的(完全)特化:template<> void f<int>(int) / template<> void f(int),其中包括以下几个关键点

  • 函数模版的特化并不引入新的(同名)名称,只是为某个模版针对特定的模版实参提供优化的算法,需要注意与函数模版重载的区别
  • 注意特化过程中的模版形参推导

虽然我们花时间讨论了函数模版的特化,但我们应该尽量避免使用函数模版的特化,主要原因有以下几点

  • 函数模版的特化不参与重载解析,会产生反直觉的效果
    template <typename T>
    void fun(T x){
        std::cout << "1\n";
    }
    
    template <typename T>
    void fun(T * x){
        std::cout << "2\n";
    }
    
    template<>
    void fun<int *>(int *x){
        std::cout << "3\n";
    }
    
    int main() {
        int x;
        fun(&x);            // 输出2
    }
    
  • 通常函数模版特化可以使用重载代替
    template <typename T>
    void fun(T x){
        std::cout << "1\n";
    }
    
    template <typename T>
    void fun(T * x){
        std::cout << "2\n";
    }
    
    
    void fun(int *x){
        std::cout << "3\n";
    }
    
    int main() {
        int x;
        fun(&x);            // 输出3
    }
    
  • 存在一些不便于重载的情况:无法建立模版形参与函数形参的关联
    template <typename T,typename Res>
    Res fun(T x){
        return Res{};			// 无法使用函数重载来代替
    }
    
    对于这种情况,我们有以下几种解决办法:
    1. 使用if constexpr解决

      template <typename T,typename Res>
      Res fun(T x){
          if constexpr (std::is_same_v<Res,int>){
      
          }
          else{
      
          }
          return Res{};
      }
      
    2. 通过引入“假函数形参”来解决

      template <typename T,typename Res>
      Res fun(T x, const Res &){
      
          return Res{};
      }
      
      template <typename T>
      int fun(T x, const int &){
          return int{};
      }
      
    3. 通过类模版特化来解决

在C++20中引入了函数模版的简化形式,可以使用auto定义模版参数类型,但在函数内部需要间接获取参数类型信息

void fun(auto x){
    decltype(x) tmp = 3;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值