C++泛型编程--函数模板浅析

C++模板

模板是C++中泛型编程的基础,一个模板就是一个创建类或蓝图或者说共识,例如我们常用的vector,list,或者是find这样的泛型函数,我们提供足够的信息将蓝图转换为特定的类或函数。本章内容:如何定义模板

  1. 定义模板
    假设我们希望编写一个函数来比较两个值大小,在实际中 ,我们可能想要定义多个函数,用来比较int,float或者我们的一些自定义类型。我们可能会定义多个重载函数。
int conmare(const string& v1, const string& v2)
{
    if (v1 < v2) return -1;
    if (v1 > v2) return  1;
    return 0;
}

int conmare(const double& v1, const double& v2)
{
    if (v1 < v2) return -1;
    if (v1 > v2) return  1;
    return 0;
}

两个函数唯一的差异在于参数类型。如果对每种希望比较的类型重复定义一样的函数体,非常之繁琐以及容易出错,并且在编写程序的时候需要确定compare的所有类型。甚至是在需要在用户提供的类型上做比较,这种策略失效了。

  1. 函数模板
    通过定义一个函数模板来代替重复性定义不同类型的函数,一个函数模板就像是一个公式用来生成特定类型的函数版本。compare的模板类型可能如下
template<typename T>
int compare(const T& v1, const T& v2)
{
    if (v1 < v2) return -1;
    if (v1 > v2) return  1;
    return 0;
}

以关键字template开始后跟一个模板参数列表。模板参数列表作用很像函数参数列表,定义若干特定类型的局部变量,但并未指出如何初始化,在运行时候调用者提供实参来进行初始化。以上文中的compare为例,我们使用T表示一个类型,T的实际类型则在变异时根据compare情况来确定。

函数模板实例化

当我们调用一个函数模板的时候,编译器通常使用函数实参数来为我们模板实参,即当我们调用compare时,编译器使用实参的类型来确定绑定到模板参数T的类型,如:

cout << compare(1, 0)<//T为int
实参类型为int,编译器会推断出模板实参为int,并绑定到模板参数T.

编译器用推断的模板参数为我们实例化一个特定版本的函数,当编译器实例化一个模板时,使用的实际模板实参代替对应的模板参数来创建出一个新的“实例”。例如给定下面的调用:

cout<1, 0)<//实例化出int compare(const int&, const int&);

vector<int>vec1{1, 2, 3}, vector<int>vec1{4, 5, 6};
cout<//实例化出int compare(vector&, vector&);

模板类型参数

我们的compare有一个模板类型参数,一般来说,我们可以将该参数类型看做类型说明符,就想内置类型一样使用,特别是,类型参数可以用来指定返回类型和函数的参数类型,以及在函数体内用于变量声明或类型转换:

//正确
templateT>
T foo(T *p)
{
    T tmm = *p;
    return tmp;
}

类型参数前必须使用关键字class 或 typename:

template<typename T, U> //错误,U之前必须加上class 或 typename
T calc(const T&, const U&)

非类型模板参数

除了定义模板类型参数,还可以在模板定义非类型参数,一个非类型参数表示一个值而非一个类型,当一个模板被实例化时,非类型参数被一个用户提供或编译器推断出的值代替,这些值必须是常量表达式,从而允许在编译器在编译时实例化模板.

例如我们编写一个compare版本处理字符串字面常量,这种字面常量是conts char的数组,由于不能拷贝一个数字,所以我们将自己的参数定义为数组的引用。第一个模板参数表示第一个数组的长度,第二个模板参数表示第二个数组的长度:

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

当我们调用这个版本compare时:

compare("hi", "mom")
编译器会用字面常量大小来代替N和 M,从而实例化模板,记住,编译器会在一个字符串字面常量末尾加入一个空字符作为终结符,因此可能会实例出以下版本:
int compare(const char (&p1)[3], const char &(p2)[4])

一个非类型参数可以是一个整形,或者是一个指向对象或函数的指针或(左值)引用,绑定到非类型整数参数的实例必须是一个常量表达式,绑定到指针或引用非类型参数的实参必须具有静态的生存期,我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参,指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
在模板定义内,模板非类型参数是一个常量值,在需要常量表达式的地方可以使用非类型参数,例如指定数组大小。

inline与constexpr 的函数模板

函数模板可以声明为inline 和 constexpr,如同非模板函数一样,inline或constexpr说明符放在模板参数列表之后,返回类型之前:

template<typename T>
inline T min(const T&, const T&); //正确

inline template<typename T>
T min(const T&, const T&);        //inline位置错误

编写类型无关的代码

我们最初的compare函数虽然简单,但它说明了泛型代码的两个重要原则:
- 模板中的函数参数是const的引用。
- 函数体中的条件判断仅使用 < 比较运算

通过将函数参数设定为const的引用,我们保证了函数可以用于不能拷贝的类型,大多类型包括内置类型和我们已经用过的标准库类型,都是允许拷贝的。但是不允许拷贝的类型也是存在的,通过将参数设定为const的引用,保证了这些类型可以用我们的compare函数来处理,而且,如果compaer用于处理大都想,这种设计策略还能使函数运行的更快。
你可能认为既使用 < 运算符 又使用>运算符 来进行比较操作会更为自然:

//期望的比较操作
if(v1 < v2) return -1;
if(v1 > v2) return -1;

但是如果编写代码只是用<运算符,我们就降低了compare函数对要处理的类型的要求,这些类型必须支持< ,但不必同时支持>, 实际上如果我们真的关心类型无关和可移植性,可能需要用less来定义我们的函数:

templateT>
int compare(const T& v1, const T& v2)
{
    if(less<T>() (v1, v2)) return -1;
    if(less<T>() (v2, v1)) return  1;
    return 0;
}

关键概念

当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的,而且模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板成员的定义也必须是可见的。
用来实例化模板的所有函数,类型,以及与类型关联的运算符声明都必须是可见的,这是由模板的用户来保证的。通过组织良好的程序结构,恰当使用头文件吗,这些要求都很容易满足,模板的设计者应该提供一个头文件,包括模板定义以及在类模板或成员定义中用到的所有名字的声明,模板用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

你可能感兴趣的:(C++)