虽然C++中引入了函数重载的功能,使得我们可以在同一作用域中声明并定义几个参数列表不同的同名函数。

但人总是不满足的,在满足了生存之后,“贪婪”与“懒惰”是人类进步的原动力。

重载函数虽然给我们提供了很大的便利,但其缺点却还是十分明显的:

1.很多时候,重载的函数仅仅是参数列表不同,代码的复用率非常低,像swap一类的功能简单且常用的函数,只要有新类型出现,就需要增加对应的函数以满足需求。

2.代码的可维护性比较低,一个不小心就可能会导致所有的重载函数都出错。

这时,C++就提出了一个方案:我们可以提供给编译器一个函数的模具,让编译器根据后边需求的类型的不同,利用模具生成对应的代码。

这个方案的核心就是我们本篇要讲的——模板

泛型编程

泛型编程是一个非常庞大的知识体系。在C++中,泛型编程的地位与面向对象所差无几。

所谓的泛型,指的是算法只要实现一遍,就可以适用于不同数据类型。

泛型编程的代表作就是我们所熟悉的STL。

1.泛型编程是代码复用的一种手段,独立于某种特定的类型,编写与类型无关,可供各种类型使用的方法等。

2.模板是泛型编程的基础,但是模板并不等于泛型编程。模板主要有两大类:函数模板与类模板。通过给定函数或者类的足够的信息,将其实例化为具体的类或者函数。

模板

模板是C++支持参数化多态的工具,是C++泛型编程的基础。使用模板可以为类或者函数声明一种模式,使得函数或者类中的某些成员的参数可以使用任意类型作为填充。

模板通常具有两种形式——类模板 函数模板

函数模板

所谓函数模板,实际上就是建立一个通用函数,对于其所使用的某些数据类型——可以是返回值类型,参数类型甚至局部变量类型,并不做出具体地指定,而采用一些标识符来作为虚拟地数据类型代替。当发生函数地调用时,编译器再根据传入的具体的实参类型而产生出与该类型相匹配的特定的函数版本。

函数模板代表了一个函数家族。函数模板与类型无关,在使用时被参数化。编译器根据其实参类型产生函数的特定类型版本。

函数模板格式:

template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表) {}

注意:模板参数名在同一模板形参列表中只能使用一次。

如:tmplate<typename T, typename T>就是错误的。

模板参数不仅仅可以使用typename修饰,还可以使用class。 

隐式实例化 

using std::cout;
using std::endl;

template<typename T>
void swap(T& p1, T& p2) {
    T tmp = p1;
    p1 = p2;
    p2 = tmp;
}

int main() {
    int a1 = 10;
    int a2 = 20;
    char s[] = "hello";
    swap(a1, a2);
    cout << "a1 = " << a1 << endl;
    cout << "a2 = " << a2 << endl;
    swap(s[0], s[3]);
    cout << s << endl;
    return 0;
}

 

以上是模板的隐式实例化的举例代码,不过,由于我们只定义了一种模板参数T,所以当用户输入的两个实参类型不同时,就会导致报错。

int main() {
    int a1 = 10;
    double a2 = 20;
    swap(a1, a2);
    return 0;
}

导致如此报错的原因是,当编译器再实例化的时候,需要根据用户输入的参数类型对代码进行推演。由于实参a1为int型,实参a2为double型,编译器通过实参a1将模板参数T推演为int型,通过实参a2将模板参数T推演为double型,这导致了模板参数T的二义性,编译器无法确定模板参数T究竟是int还是double。

而编译器在模板中是不会进行类型转换操作的...

对于这样的情况有两(三)种处理方式

0.改模板

template<typename T1, typename T2>
void swap(T1& p1, T2& p2) {
    T1 tmp = p1;
    p1 = p2;
    p2 = tmp;
}

int main() {
    int a1 = 10;
    double a2 = 20.0;
    swap(a1, a2);
    cout << "a1 = " << a1 << endl;
    cout << "a2 = " << a2 << endl;
    return 0;
}

 有一定可行性,但感觉不怎么靠谱,所以凑一下——两种处理方法提供了三个方案没问题吧?

我原本感觉这是最不靠谱的一种方案,然而在经过多次尝试后却发现......由于我这里所举例子的特殊性——引用,反而导致这种方法是最靠谱,唯一不会报错的......

1.用户手动强制转换

由于强制类型转换后产生的值属于右值,无法被左值引用引用。

2.使用显式实例化

在不修改模板的前提下,这两种方法编译器一直会有报错、或者运行结果不正确

PS.实验时使用的Visual Studio 2019。

这件事情告诉我们,一定要守规矩

显式实例化

void swap(T& p1, T& p2) {
    T tmp = p1;
    p1 = p2;
    p2 = tmp;
}

int main() {
    int a1 = 10;
    int a2 = 20;
    swap<int>(a1, a2);
    cout << "a1 = " << a1 << endl;
    cout << "a2 = " << a2 << endl;
    return 0;
}

我们可以在模板函数调用时,在其函数名后使用一对尖括号<>来表明我们需要该函数的模板参数实例化为哪种数据类型。这样的方法被称为显式实例化。

在显式实例化的条件下,如果参数类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功则会报错。

模板参数匹配原则

1.一个非模板函数可以与一个同名的函数模板同时存在,并且该函数模板还可以被实例化为这个非模板函数。

int add(int n1, int n2) {
    cout << "no template" << endl;
    return n1 + n2;
}

template<class T> 
T add(T n1, T n2) {
    cout << "template" << endl;
    return n1 + n2;
}

int main() {
    add(3, 4);
    add<int>(3, 4);
    return 0;
}

 

2.对于非模板函数与其同名的模板函数,如果其它条件都相同,在调用时会优先调用非模板函数而不会使用模板推演出实例。如果模板可以产生更加匹配的函数,那么会选择模板。

int add(int n1, int n2) {
    cout << "no template" << endl;
    return n1 + n2;
}

template<class T1, class T2> 
T1 add(T1 n1, T2 n2) {
    cout << "template" << endl;
    return n1 + n2;
}

int main() {
    add(3, 4.0);
    add(3, 4.0);
    return 0;
}

3.模板函数并不支持自动类型转换,但普通函数可以。

类模板

类模板的定义格式

template<typename T1, typename T2, ... ,typename Tn>
class 类模板名 {
    //类成员定义
}

类模板成员函数的类外定义

template<class T> 
class Array {
private:
    T* _p;
    int _cap;
    int _len;
public:
    Array(size_t cap = 10)
        :_len(0)
        , _cap(cap)
        , _p(new T[cap])
    { }

    ~Array() {
        delete[] _p
    }

    int size();
};

template<class T>
int Array<T>::size() {
    return len;
}

当我们需要将类中的某个成员函数在类外定义时,需要加上模板参数列表。

类模板的实例化

类模板的实例化与函数模板的实例化不同,类模板实例化必须显式声明模板参数类型,即在类模板名字后跟<>,然后将我们所需要实例化的类型放在<>之中。

类模板不是真正的类,但是在使用尖括号<>指明模板参数的具体类型后,它就成为了真正的类,此时实例化出来的就是类的对象。

Array<int> arr1;
Array<double> arr2;

//Array是类模板
//Array<int>与Array<double>是类
//arr1, arr2是类实例化出来的对象

更多推荐

C++之模板