C++模板详解(一):函数模板的概念和特性

函数模板是指这样的一类函数:能够用多种不一样数据类型的参数进行调用,表明了一个函数家族。它的外表和普通的函数很类似,惟一的区别就是:函数中的有些元素是未肯定的,这些元素将在使用的时候才被实例化。先来看一个简单的例子:ios

1、定义一个简单的函数模板

下面的这个例子就定义了一个模板函数,它会返回两个参数中最大的那一个:函数

// 文件:"max.hpp"
template<typename T>
inline const T& max(const T& x, const T& y)
{
    return x < y ? y : x;
}

这个函数模板定义了一个“返回两个值中最大者”的函数家族,而参数的类型尚未肯定,用类型模板参数T来肯定。模板参数须要使用以下的方式来声明:spa

template< 模板参数列表 >

在这个例子中,模板参数列表为:typename T。关键字typename引入了T这个类型模板参数。固然了,可使用任何标识符做为类型模板参数的名称。咱们可使用任何类型(基本数据类型、类类型)来实例化该函数模板,只要所使用的数据类型提供了函数模板中所须要的操做便可。例如,在这个例子中,类型T须要支持operator <,由于a和b就是经过这个操做符来比较大小的。设计

鉴于历史缘由,也可使用关键字class来取代typename来定义类型模板参数,然而应该尽量地使用typename指针

2、使用函数模板

下面的程序使用了上面定义的这个函数模板:code

#include <iostream>
#include <string>
#include "max.hpp"

using namespace std;

int main(int argc, char *argv[])
{
    cout << max(4, 3) << endl; // 使用int类型实例化了函数模板,并调用了该函数实例。
    cout << max(4.0, 3.0) << endl; // 使用double类型实例化了函数模板,并调用了该函数实例。
    cout << max(string("hello"), string("world")) << endl; // 使用string类型实例化了函数模板,
                                                           // 并调用了该函数实例。
    return 0;
}

一般而言,并非把模板编译成一个能够处理任何类型的单一实体,而是针对于实例化函数模板参数的每种类型,都从函数模板中产生出一个独立的函数实体。所以,针对于每种类型,模板代码都被编译了一次。这种用具体类型代替模板参数的过程,叫作模板的实例化。它产生了一个新的函数实例(与面向对象程序设计中的实例化不一样)。对象

若是试图基于一个不支持模板内部所使用的操做的类型实例化一个模板,那么将会引起一个编译期错误:编译器

std::complex<double> c1, c2;
max(c1, c2); // 编译错误:std::complex并不支持运算符<

因此说:模板被编译了两次,分别发生于:string

  • 模板实例化以前,查看语法是否正确,此时可能会发现遗漏的分号等。
  • 模板实例化期间,检查模板代码, 查看是否全部的调用都有效。此时可能会发现无效的调用,例如实例化类型不支持某些函数调用等。

因此这引起了一个重要的问题:当使用函数模板而且引起模板实例化时,编译器必须查看模板的定义。事实上,这就不一样于普通的函数,由于对于普通的函数而言,只要有函数的声明(甚至不须要定义),就能够顺利地经过编译期。io

3、函数模板实参推断

当咱们为某些实参调用一个函数模板时,模板参数能够由咱们所传递的实参来决定。

注意:函数模板在推断参数类型时,不容许自动类型转换,每一个类型模板参数都必须正确的匹配。

template<typename T>
inline const T& max(const T& x, const T& y)
{
    return x < y ? y : x;
}

int main()
{
    // 不能这样调用:
    // max(10, 20.0); // 错误,由于函数模板中的类型推断拒绝隐式类型转换
    // 这是由于,没法肯定到底应该使用哪一个参数类型来实例化这个模板函数。
    // 因此,C++拒绝了这种作法。 可用的解决方案:
    ::max(static_cast<double>(10), 20.0); // OK,由于两个参数都为double。
    ::max<double>(10, 20.0); // OK, 显示指定参数,这样能够尝试对参数进行类型转换。
    return 0;
}

注意:模板实参推断并不适合返回类型。由于返回类型并不会出如今函数调用参数的类型里面。

因此,必需要显示地指定返回类型:

template<typename T1, typename T2, typename RT>
inline RT func()
{
    // ...
    return RT();
}

int main(int argc, char *argv[])
{
    func<int>(); // 必须这样显示地指定返回类型才能够,没法进行自动类型推断。
    return 0;
}

4、函数模板的重载

和普通的函数同样,函数模板也能够被重载。在下面的例子中,一个非模板函数能够和一个同名的函数模板同时存在,这称为函数模板的特化。并且该函数模板还被实例化为这个非模板函数。

// #1
inline const int& max(const int& a, const int& b)
{
    return a < b ? b : a;
}

// #2
template<typename T>
inline const T& max(const T& a, const T& b)
{
    return a < b ? b : a;
}

// #3
template<typename T>
inline const T& max(const T& a, const T& b, const T& c)
{
    return max(max(a, b), c);
}

int main(int argc, char *argv[])
{
    /*01*/max(7, 42, 68);       // 调用#3
    /*02*/max(7.0, 6.0);        // 调用#2
    /*03*/max('a', 'b');        // 调用#2
    /*04*/max(7, 42);           // 调用#1
    /*05*/max<>(7, 42);         // 调用#2
    /*06*/max<double>(7, 42);   // 调用#2可是没有推断参数
    /*07*/max('a', 42.7);       // 调用#1
    return 0;
}

总结以下:

  • 对于非模板函数和同名的函数模板,若是其它条件都是相同的话,那么在调用的时候,重载解析过程当中会优先调用非模板函数,而不会实例化模板(04)。
  • 若是模板能够产生一个具备更好匹配的函数,那么将选择模板(02, 03)。
  • 还能够显示地指定一个空的模板参数列表,告诉编译器:必须使用模板来匹配(05)。
  • 因为函数模板拒绝隐式类型转换,因此当全部的模板都没法匹配,可是发现能够经过强制类型转换来匹配一个非模板函数时,将调用那个函数(07)。

5、函数模板重载的注意事项

在重载函数模板时,请谨记:将对函数声明的改变限制在如下两种状况中:

  • 改变参数的数目
  • 显示指定模板的参数(即函数模板特化)

不然,极可能会致使非预期的结果,例如在下面的例子中,模板函数是使用引用进行传参的,然而在其中的一个重载中(其实是针对char*进行的特化),却使用了值传递的方式,这将会致使不可预期的结果:

template<typename T>
inline const T& max(const T& a, const T& b)
{
    return a < b ? b : a;
}

// #2: 存在隐患,由于其它的重载都是以引用传递参数,而这个重载版本
// 却使用了值传递,不符合上面介绍的须要遵照的两个可变条件。
inline const char* max(const char* a, const char* b)
{
    return std::strcmp(a, b) < 0 ? b : a;
}

template<typename T>
inline const T& max(const T& a, const T& b, const T& c)
{
    // 这里对max(a, b)的调用,若是调用了函数#2,
    // 那么将会返回一个局部的值,若是刚好这个局部的值
    // 又比c大,那么将会返回一个指向局部变量的指针,
    // 这是很危险的(非预期的行为)。
    return max( max(a, b), c );
}

int main()
{
    char str1[] = "frederic";
    char str2[] = "anica";
    char str3[] = "lucas";

    char* p1 = str1;
    char* p2 = str2;
    char* p3 = str3;

    // 这种用法是错的,这是由于:
    // max(a, b)返回的是一个指针,这个指针是一个局部的对象,
    // 而且这个局部的对象颇有可能会被返回。
    auto result = max(p1, p2, p3);
    return 0;
}
相关文章
相关标签/搜索