函数模板

什么是函数模板

C++的函数模板提供了一种方法,让编译器根据你写的模板,来自动按需生成函数。这个模板就称之为函数模板。函数模板格式如下:

template <typename AnyType>
void MyFunctionTemplate(AnyType &param)
{
}

这里AnyType就是任意数据类型,既包括语言内置的基础数据类型(charintdouble),又包括自定义的数据类型,比如类,结构体,枚举,数组等。

为什么需要函数模板

假设我需要一个方法,打印给定数据类型的长度。第一时间想到的是重载函数来实现这个功能。

重载函数的缺陷

  • 函数定义可能存在冗余

    假定printTypelength方法在一个单独的模块内,将被我编译为动态库供别人使用,那么我就必须提前把所有要处理的类型都给实现了。如果我的应用程序如下:

int main(void)
{
    char c;
    printTypelength(c);
    return 0;
}

那么我代码中实际仅需要针对char的重载函数即可,其他几十个重载函数完全用不上,可执行程序的就会变小了。函数重载并未按需实现,而是一股脑的定义一堆你需要的重载函数。

  • 可重用性不高

    假设某天我突然定义了一个新的自定义数据类型,想要使用模块的printTypelength函数,这时我不得不在printTypelength源码里面新增对应的重载函数。如果我作为这个模块的维护者,需要没完没了的新增自定义数据类型的重载函数。

函数模板和函数模板实例概念

在进一步了解函数模板前,需要先知道函数模板函数模板实例这个概念。函数模板概念最开始介绍了,函数模板实例又是什么?可以类比类的定义和类的实例(对象):

//类的定义
class MyClass
{
}
//类的实例
MyClass a;

//函数模板的定义
template <typename T>
void MyFunction(T a)
{
}
//函数模板的实例(这种称为隐示实例化)
MyFunction<int>(1);

函数模板实例就是函数模板被指定为具体类型时的代码。 类的定义并不会导致内存的分配,类的实例才会进行内存分配。同样的,函数模板的定义并不会立即触发编译器生成函数,而是在代码中函数模板被指定为具体类型时,编译器就会生成具体的函数。

隐示实例化和显示实例化

//函数模板的定义
template <typename T>
void MyFunction(T a)
{
}

template void MyFunction(int a);        //显式实例化,写法一
template void MyFunction<int>(int a);   //显式实例化,写法二

int main()
{
    MyFunction(1);      //隐式实例化,写法一
    MyFunction<int>(1); //隐式实例化,写法二
}
  • 隐式实例化通过对模板函数的调用,触发函数生成。
  • 显式实例化,通过特定的语法规则,提前强制触发函数生成。

他们都可以把省略,由编译器自动推导出类型。需要注意的是显式实例化不要实现函数体,因为它会触发编译器根据函数模板,自动生成的。

函数模板的具体化

前面函数模板不管是显式实例化还是隐式实例化,都会依照函数模板的函数体来生成函数,生成的这些函数执行逻辑完全是一样的。但是有时候,处理不同类型时,处理逻辑期望不一样,也就是函数体不一样。例如实现加法的函数模板,对于基本类型来说,可以用一样的模板,如果传入的类型是自定义的类呢?这时候就需要函数模板的具体化来实现。

class MyClass
{
public:
    MyClass(int x, int y)
    {
        this->x = x;
        this->y = y;
    }
    int x;
    int y;
};

//函数模板
template <typename T>
T Add(T a, T b)
{
    return a + b;
}

//函数模板具体化,写法一
template <> 
MyClass Add(MyClass a, MyClass b)
{
    return MyClass(a.x + b.x, a.y + b.y);
}

//函数模板具体化,写法二
template <> 
MyClass Add<MyClass>(MyClass a, MyClass b)
{
    return MyClass(a.x + b.x, a.y + b.y);
}

可以看到具体化就是覆盖了原来的模板里面的函数体。和实例化不同的地方除了需要实现自己的函数体外,还有就是template后面多了一对<>号。实际上上面的例子可以通过对自定义类型操作符重载来实现。上面例子中如果MyClass类里面对+号操作符重载,同样使用实例化模板函数也可以实现功能。但是如果函数里面的逻辑更复杂,并且实现的逻辑也不是某个操作符的逻辑,那就必须要用函数模板具体化了。

函数模板重载和默认参数,以及默认类型

  • 函数模板重载

函数模板也是可以重载的,规则和函数重载一样。例如:

template <typename T>
void foo(T a)
{
}

template <typename T>
void foo(T a , int b)
{
}

foo<int>(1);
foo<int>(1, 2);
  • 函数模板中的参数的默认值
template <typename T>
void foo(T a , int b = 0)
{
}

int main()
{
    foo<int>(1);
    foo<int>(1, 2);
    return 0;
}
  • 默认类型
template <typename T = char>
void foo(T a)
{
}

int main()
{
    foo(1);
    foo(0.3);
    return 0;
}

函数模板中的默认类型是C++11标准才引入的,之前标准不支持。目前我还没搞清楚这种写法有什么用处。上面2个例子,让编译器自动推导一个被推导为int,一个被推导为double。

函数模板和函数重载

我个人觉得这两者最终都是被统一为函数重载,也就是编译器在编译过程中根据特征标,按照一定的规则,把函数重命名了一下。函数模板无非是多了一个根据指定类型或者自动推导(未显示指定类型时)出的类型,生成函数而已,这个过程中还是有根据特征标重命名函数。

匹配顺序

对于同一个函数名,非模板函数,模板函数是可以同时存在的。函数调用时,会按照匹配度进行调用:

非模板函数 > 实例化或者具体化 > 函数模板
void foo(int) //①
{
}

template <typename T> //②
void foo(T a)
{
}

template void foo(double a); //③

template <>
void foo(short) //④
{
}

int main()
{
    int a = 1;
    double b = 2.3;
    short c = 4;
    long d = 4;
    foo(a); //调用①
    foo(b); //调用③
    foo(c); //调用④
    foo(d); //调用②

    return 0;
}
posted @ 2024-06-30 10:36  thammer  阅读(3)  评论(0编辑  收藏  举报