C++模版:深入使用函数模版

上文函数模版初探介绍了函数模版的基本使用,本文深入探讨函数模版。

函数模版参数的类型转换

函数模版有多个同类型的形参,使用过程中会碰到类型转换问题。

下面是一个加法函数模版:

template <typename  T>
T add2(T a, T b) {
  std::cout << "T add2(T a, T b)" 
            << std::endl;  
  return a + b;
}
复制代码

使用隐式实例化,2个实参的类型必须相同,否则编译出错No matching function for call to add

使用显示实例化,2个实参的类型可以不同,但是会发生隐式类型转换。

void test1() {
    // 2个实参类型相同,可以使用隐式实例化
    int a1 = 1;
    int b1 = 2;
    add2(a1, b1);
    
    // 2个实参类型不同,不能使用隐式实例化
    int a2 = 1;
    short b2 = 1;
    // No matching function for call to 'add2'
    // add2(a2, b2); 
  
    // 显示实例化, short被隐式转换成int
    add2<int>(a2, b2);      
}
复制代码

函数模版重载

函数模版跟普通函数一样支持重载。函数参数列表中,参数不一定是泛型,也可以是具体的类型。

比如,加法函数模版中,增加3个数相加的函数模版。第3个参数,可以是泛型,也可以是具体的double类型。

template <typename  T>
T add2(T a, T b, T c) {
    std::cout << "T add2(T a, T b, T c)" 
              << std::endl;
    return a + b + c;
}

template <typename  T>
T add2(T a, T b, double c) {
    std::cout << "T add2(T a, T b, double c)" 
      	      << std::endl;
    return a + b + c;
}
复制代码

函数模版的局限性及解决方案

函数模版很可能无法处理某些类型。

比如下面的例子,如果T是自定义类FTPerson,则无法正常运行。

template <typename  T>
bool equal(T &a, T &b) {
    return a == b;
}

class FTPerson {
public:
    int age;
};
复制代码

一种解决方案是,为特定类型提供具体化的函数定义,这称之为显示具体化(explicit specialization)。

显示具体化的原型和定义:

template<> bool equal<FTPerson>(FTPerson &p1, FTPerson &p2);

template<> bool equal<FTPerson>(FTPerson &p1, FTPerson &p2) {
    std::cout << "equal(FTPerson &p1, FTPerson &p2)" 
      	      << std::endl;
    return p1.age == p2.age;
}
复制代码

编译器会选择最合适的显示具体化函数。

void test3() {
    FTPerson p1;
    FTPerson p2;
    
    equal(p1, p2);	// 调用的是具体化函数
}
复制代码

编译器选择哪个函数

调用函数时,如果有多个函数或者模版符合要求:

  • 函数名称相同
  • 实参与形参个数相同,并且类型相同或者可以隐式转换

编译器必须选择一个最佳的函数。以下匹配规则,优先级从高到低:

  • 函数完全匹配,按照下面优先级匹配
    • 普通函数
    • 模版的显示具体化函数
    • 函数模版
  • 提升转换,比如char转换为intfloat转换为double
  • 标准转换,比如char转换为floatint转换为charlong转换为double

下面是5个函数或者模版,编号是1到5。使用不同编号的函数和测试用例,验证匹配规则。

// #1 普通函数
void eat(int a) {
    std::cout << "eat(int a)" 
      	      << std::endl;
}

// #2 普通函数
void eat(char a) {
    std::cout << "eat(char a))" 
      	      << std::endl;
}

// #3 普通函数
void eat(float a) {
    std::cout << "eat(float a)" 
      	      << std::endl;
}

// #4 函数模版
template<typename T> void eat(T a) {
    std::cout << "template void eat(T a)" 
              << std::endl;
}

// #5 模版的显示具体化函数
template<> void eat(int a) {
    std::cout << "template void eat(int a)" 
      	      << std::endl;
}
复制代码

测试用例1:

void test4() {
    int a = 1;
    eat(a);
}
复制代码

下面情况都是完全匹配,输出结果如下:

  • 使用#1#4#5,则编译器选择普通函数#1,输出日志eat(int a)
  • 使用#4#5,则编译器选择显示具体化函数#5,输出日志template void eat(int a)

下面情况是完全匹配优先级高于提升转换或者标准转换,输出结果如下:

  • 使用#2#3#4,则编译器选择函数模版#4,输出日志template void eat(T a)

测试用例2:

void test5() {
    char a = 'a';
    eat(a);
}
复制代码

下面情况是提升转换优先级高于标准转换,输出结果如下:

  • 使用#1#3,则编译器选择提升转换#1,输出日志eat(int a)

总结

  • 使用函数模版,要注意隐式类型转换
  • 函数模版支持重载
  • 函数模版不能处理所有的类型,可以为特定类型提供具体化的函数定义
  • 如果多个函数或者模版符合要求,编译器会根据优先级,选择最合适的函数

阅读原文