C++ Primer Plus书之--C++函数模版及模板重载

函数模板

函数模板允许以任意类型的方式来定义函数, 例如:可以这样建立一个交换模板(交换两个参数的数值)

// 建立一个模板, 并将类型命名为AnyType, 关键字template是必须的
// 类型名AnyType可以任意选择, 只要遵守C++命名规则即可. 例如T.
// typename也是必须的, 但是可以用class进行替换
template <typename AnyType>
// 定义一个模板函数
void swap(AnyType & a, AnyType & b)
{
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}

模板并不创建任何函数, 只是告诉编译器如何定义函数. 需要交换int的函数时, 编译器将按模板模式创建这样的函数, 并用int代替AnyType. 

看一个简单的例子:

// 模板函数
#include <iostream>
using namespace std;

// 定义模板函数原型
// 将类型定义为T
template <typename T>
void Swap(T & a, T & b);

int main()
{
	int i = 10;
	int j = 20;
	cout << "i = " << i << ", j = " << j << endl;
	cout << "using compiler-generated int swapper: " << endl;
	Swap(i, j);
	cout << "Now i = " << i << ", j = " << j << endl;
	
	double x = 1.23;
	double y = 2.34;
	cout << "x = " << x << ", y = " << y << endl;
	cout << "using compiler-generated int swapper: " << endl;
	Swap(x, y);
	cout << "Now x = " << x << ", y = " << y << endl;
	return 0;
}

template <typename T>
// 定义一个模板函数
void Swap(T & a, T & b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

程序运行结果:

注意: 函数模板不能缩短可执行程序, 对于上面的demo, 最终仍将由两个独立的函数定义, 最终的代码不包含任何模板, 而至包含了为程序生成的实际函数. 使用模板的好处是, 使得生成多个函数定义更简单, 更可靠. 通常将模板放到头文件中, 并在需要使用模板的文件中包含头文件.

模板重载

模板重载也是要根据参数列表进行重载, 并且模板函数的参数列表不一定都是泛型.

看一个demo:

// 模板函数
#include <iostream>
using namespace std;

// 定义一个模板原型
template <typename T>
// 这里是由于要交换内容, 所以使用引用变量
void Swap(T& a, T& b);

// 模板原型, 和上面的Swap进行了重载, 这里因为是要接收数组所以使用的是指针
template <class T>
void Swap(T* a, T* b, int n);

void Show(int a[]);
const int Lim = 7;

int main()
{
    int i = 10, j = 20;
    cout << "i = " << i << ", j = " << j << endl;
	cout << "using compiler-generated int swapper: " << endl;
	Swap(i, j);
	cout << "Now i = " << i << ", j = " << j << endl;  
	
	int a1[Lim] = {1, 2, 3, 4, 5, 6, 7};  
	int a2[Lim] = {11, 22, 33, 44, 55, 66, 77};
	cout << "Original arrays : " << endl;
	Show(a1);
	Show(a2);
	Swap(a1, a2, Lim);
	cout << "Swap arrays" << endl;
	Show(a1);
	Show(a2);
	return 0;
}

template <typename T>
void Swap(T& a, T& b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

template <typename T>
void Swap(T a[], T b[], int n)
{
    T temp;
    for(int i = 0; i < n; i++)
    {
        // a[i]也可以
        temp = *(a + i);
        a[i] = b[i];
        b[i] = temp;
    }
}

void Show(int a[])
{
    for(int i = 0; i < Lim; i++)
    {
        cout << a[i] << ", ";   
    }
    cout << endl;
}

程序运行结果:

为特定类型提供具体化的模板定义

显示具体化

假设定义了如下结构:

struct jbo

{
    char name[40];
    double salary;
    int floor;
}

假设需要完成如下的功能: 交换两个结构体的salary和floor成员, 而不交换name成员, 但Swap()的参数将保持不变 , 因此无法使用模板重载来提供其他代码.

因此C++提供了一个具体化函数定义--显示具体化(explicit specialization), 其中包含所需的代码, 当编辑器找到与函数调用匹配的具体化定义时, 将使用该定义, 而不再寻找模板:

具体化方法:

1.对于给定的函数名, 可以有非模板函数, 模板函数和显示具体化模板函数以及它们的重载版本

2.显示具体化的原型和定义应该以template<>打头, 并通过名称来指出类型.

3.具体化优先于常规模板, 而非模板函数优先于具体化和常规模板.

下面是用于交换job结构的非模板函数, 模板函数和具体化的原型.

// 非模板函数, 原型可以不写变量名
void Swap(job &, job &);


// 模板原型T和&之间的空格可选
template <typename T>
void Swap(T&, T &);


// 具体化原型
// Swap<job>中的job是可选的, 因为函数的参数类型表明, 这是job的一个具体化
template<> void Swap<job>(job &, job &);

正如上面说的, 如果有多个原型, 则编译器在选择原型时, 非模板版本最高, 其次是显示具体化, 最后是模板原型.

看一个使用模版先后顺序的demo:

// 具体化模板
#include <iostream>
using namespace std;

// 普通模板原型
template <typename T>
void Swap(T & a, T& b);

struct job
{
    char name[40];
    double salary;
    int floor;
};

// 具体化模板原型
template<> void Swap<job>(job& j1, job& j2);
void Show(job& j);

int main()
{
    // 规定了精确小数点后两位
    cout.precision(2);
    cout.setf(ios::fixed, ios::floatfield);
    int i = 10, j = 20;
    cout << "i = " << i << ", j = " << j << endl;
    
    cout << "Using compiler generated int swapper:" << endl;
    // 使用的是普通模板函数
    Swap(i, j);
    cout << "Now i = " << i << ", j = " << j << endl;
    
    job sue = {"Tom", 120000.23, 7};
    job sideny = {"Jack", 230000.45, 9};
    
    cout << "Befor job swapping" << endl;
    Show(sue);
    Show(sideny);
    // 由于sue和sideny是job类型的数据结构
    // 因此调用的是具体化模板
    Swap(sue, sideny);
    
    cout << "After job swapping" << endl;
    Show(sue);
    Show(sideny);
    return 0;
}

template<typename T>
void Swap(T& a, T& b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

// 具体化模板
template<> void Swap<job>(job& j1, job& j2)
{
    double t1;
    int t2;
    t1 = j1.salary;
    t2 = j1.floor;
    
    j1.salary = j2.salary;
    j1.floor = j2.floor;
    
    j2.salary = t1;
    j2.floor = t2;
}

void Show(job& j)
{
    cout << j.name << ", salary = " << j.salary << ", floor = " << j.floor << endl;
}

程序运行结果:

实例化和具体化

记住在代码中包含函数模板本身并不会生成函数定义, 它只是一个用于生成函数定义的方案. 编译器使用模板为特定类型生成函数是由定义时, 得到的是模板实例(instantiation). 例如:上面的例子当中, 函数调用Swap(i, j)导致编译器生成Swap()的一个实例, 该实例使用int类型. 模板并非函数定义, 但是使用int的模板实例是函数定义. 这种实例化方式被称为隐式实例化(implicit instantiation), 因为编译器之所以知道需要进行定义, 是由于程序调用Swap()函数时提供了int参数.

对应的显示实例化的语法是:

// 显示实例化
template void Swap<int>(int , int);

注意和具体化的区别:
// 具体化
template<> void Swap<int>(int &, int &);

他们的区别就是具体化声明需要在关键字template后包括<>, 而显示实例化则没有.

警告:试图在同一个文件(或转换单元)中使用同一种类型的显示实例和显示具体化将出错.

还可以通过在程序中使用函数来创建显示实例化, 例如:

template <class T>
T Add(T a, T b)
{
    return a + b;
}
...
int m = 6;
double x = 10.2;
// 显示实例化, 这里m会强制转换成double类型
cout << Add<double>(m, x) << endl;

如果对Swap做类似的处理:

int m = 6;
double x = 10.2;
Swap<double>(m, x);

这样也会为double生成一个显示实例化, 不幸的是, 这些代码不管用, 因为第一个形参是double&, 不能指向int变量m.

这里对显示实例化和显示具体化等做一下总结:

...
// 模板原型, T和&之间的空格可选
template <class T>
void Swap(T&, T &);

// 显示具体化
template<> void Swap<job>(job &, job &);

int main(void)
{
    // 为char类型显示实例化
    // 编译器看到这行代码, 会使用模板定义来生成Swap()的char版本
    template void Swap<char>(char&, char &);
    short a, b;
    ...
    // 隐式实例化short类型的Swap
    Swap(a, b);
    
    job j1, j2;
    ...
    // 调用显示具体化job模板
    Swap(j1, j2);
    
    char g, h;
    ...
    // 调用显示实例化char类型的Swap, 在第一行代码显示实例化过
    Swap(g, h);
}

编译器选择使用哪个函数版本
对于函数重载, 函数模板和函数模板重载, C++提供了一个叫重载解析(overloading resolution)的规则
1.创建候选函数列表, 其中包含于被调用函数的名称相同的函数和模板函数.
2.使用候选函数列表创建可行函数列表. 
3.确定是否有最佳的可行函数, 如果有, 就使用它, 否则该函数调用出错.

例如有如下的调用:

may('B');

首先编译器将寻找名称为may()的函数和函数模板, 然后寻找那些可以用一个参数调用的函数, 例如下面这些函数原型:

void may(int); // #1
// 带有一个默认参数, 所以也可以使用单个参数进行调用
float may(float, float = 3); // #2
void may(char); // #3
char* may(const char*); // #4
char may(const char &); // #5
template<class T> void may(const T &); // #6
template<class T> void may(T *); // #7

注意只考虑参数列表, 不考虑返回值, 其中的#4, #7不可行, 因为整形类型不能被隐式的转换为指针类型, 接下来要确定那个是最佳的, 按照以下顺序来确定:
1.完全匹配, 但是常规函数优先于模板
2.提升转换(例如: char和short自动转换为int, float自动转换为double);
3.标准转换*(例如: int转换为char, long转换为double)
4.用户自定义的转换.

看一个demo:

#include <iostream>
using namespace std;

// 普通模板, 显示数组, 记得要传递进来数组的长度
template<typename T>
void ShowArray(T arr[], int n);

// 普通模板, 参数是指针
template<typename T>
void ShowArray(T * arr[], int n);

// 结构体
struct debts
{
    char name[50];
    double amount;
};

int main()
{
    int things[6] = {1, 2, 3, 4, 5, 6};
    struct debts mr_E[3] = 
    {
        {"Ima", 2400.0},
        {"Ura", 1300.0},
        {"Iby", 1800.0}
    };
    
    // 创建指针数组
    double* pd[3];
    // 将指针数组中的每个指针指向mr_E中各个元素的amount
    for(int i = 0; i < 3; i++)
        pd[i] = &mr_E[i].amount;
        
    cout << "Listing Mr. E's counts of things:" << endl;
    // 调用模板函数 int型的模板函数, 参数是数组的A
    ShowArray(things, 6);
    
    cout << "Listing Mr. E's debts:" << endl;
    // 使用的是指针的那个模板函数B
    ShowArray(pd, 3);
    return 0;
}

template<typename T>
void ShowArray(T arr[], int n)
{
    cout << "template A" << endl;
    for(int i = 0; i < n; i++)
        cout << *(arr + i) << " ";
    cout << endl;
}

template<typename T>
void ShowArray(T* arr[], int n)
{
    cout << "template B" << endl;
    for(int i = 0; i < n; i++)
        cout << *arr[i] << " ";
    cout << endl;
}

程序运行结果为:

其中pd是一个double*数组的名称, 这与模板A匹配:
其中T被替换为类型double*, 这种情况下, arr[i]将显示的是pd数组的内容也就是3个地址, 
同时该函数调用也与模板B匹配
这时候, T被替换为double, 而*arr[i]则显示的是数组内容指向的double值, 
在这两个模板中, 模板B更具体, 因为B中指出了数组内容是指针, 因此被使用(按我的理解就是B模板的参数列表与pd更为复合).

如果将模板B从程序中删除, 则编译器会使用模板A来显示pd内容, 因此会显示的是地址而不是值

自己选择

看一个dmeo:

// 自己选择模板
#include <iostream>

using namespace std;

// 模板
template<class T>
T lesser(T a, T b)
{
	return a < b ? a : b;
}

int lesser(int a, int b)
{
	a = a < 0 ? -a : a;
	b = b < 0 ? -b : b;
	return a < b ? a : b;
}

int main()
{
	int m = 20;
	int n = -30;
	double x = 12.34;
	double y = 56.78;
	
	// 优先使用普通方法
	cout << lesser(m, n) << endl;
	// 使用double类型的模板方法
	cout << lesser(x, y) << endl;
	// lesser<>指明了要使用模板方法, 因为参数是int型的, 所以使用的是int型模板方法
	cout << lesser<>(m, n) << endl;
	// 强制使用模板方法, 会把x, y先转化成int型然后使用
	cout << lesser<int>(x, y) <<endl;
}

运行结果:

程序中值得注意的地方都在注释中写明白了

模板函数更进一步

看如下的模板:

template<class T1, class T2>
void ft(T1 x, T2 y)
{
    ...
    ?type? xpy = x + y;
    ...
}

这种情况下xpy应该是什么类型呢?
由于不知道ft将如何使用,  因此我们也不知道x + y是怎么类型, 
在C++11中引入了关键字decltype来解决这个问题, 例如:

int x;
// 将y声明成x的类型
decltype (x) y;

因此上面例子中的代码就可以写成:

decltype(x + y) xpy;
xpy = x + y;

或者也可以合并成一句:

// 定义xpy为 x + y的类型
decltype(x + y) xpy;

因此前面的那个模板函数可以写成:

template<class T1, class T2>
void ft(T1 x, T2 y)
{
    ...
    decltype(x + y) xpy = x + y;
    ...
}


decltype为确定类型, 编译器会遍历一个核对表, 例如有如下声明:

decltype(expression) var;

1.如果expression是一个没有用()括起来的标识符, 则var类型与该标识符相同, 包括const等限定符:
   

    double x = 5.5;
    doubel y = 7.7;
    double & rx = x;
    const double* pd;
    // w是double类型
    decltype(x) w;
    // u是double& 类型, 也就是double的引用类型
    decltype(rx) u = y;
    // v是double指针类型
    decltype(pd) v;


2.如果expression是一个函数调用, 则var的类型与函数的返回类型相同;
   

    // 函数原型
    long indeed(int);
    // m是long类型
    decltype(indeed(3)) m;

3.如果expression是一个用括号括起来的标识符, 则表示引用

    double xx = 3.3;
    // w是一个double
    decltype(xx) w = xx;
    // r2是一个double&
    decltype((xx)) r2 = xx;


4.如果前面的条件都不满足, 则var的类型与expression的类型相同:

    int j = 3;
    int& k = j;
    int& n = j;
    // i1是int型
    decltype(j + 6) i1;
    // i2是long型
    decltype(100L) i2;
    // i3是int型, 虽然k, n都是引用, 但是k + n不是引用, 而是两个int的和
    decltype(k + n) i3;


    
如果需要多次声明, 可结合typedef和decltype一起使用

template<class T1, class T2>
void ft(T1 x, T2 y)
{
    ...
    typedef decltype(x + y) xpytype;
    xpytype xpy = x + y;
    xpytype arr[10];
    xpytype& rxy = arr[2];
    ...
}

另一种函数声明语法(C++后置返回类型)

template<class T1, class T2>
?type? get(T1 x, T2 y)
{
    ...
    return x + y;
}


这种情况, 好像可以将返回类型设置为decltype(x + y), 但不行的是, 此时还未声明参数x和y, 他们不在作用于内. 必须在声明参数后使用decltype. 
为此C++新增了一种声明和定义函数的语法:
对于下面的原型:
double h(int x, float y);
使用新增的语法可编写成这样:
auto h(int x, float y) -> double;
这将返回类型移到了参数声明后面. ->double被称为后置返回类型(trailing return type). 其中auto是一个占位符, 表示后置返回类型提供的类型. 这种语法也可以用于函数定义:

auto h(int x, float y) -> double
{
    ...
}

通过结合使用这种语法和decltype, 便可给gt()指定返回类型, 如下:

template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{
    ...
    return x + y;
}

猜你喜欢

转载自blog.csdn.net/c1392851600/article/details/84780963