6.2 函数模板
不考虑类型的话,许多函数的函数体都是相同的。例如,使用类型相同的数组对另一个数组进行初始化就使用了相同的函数体,其中包含的基本代码如下:
for (i = 0; i < n; ++i)
a[i] = b[i];
许多程序员使用简单的宏自动完成上述工作:
#define COPY(A, B, N) \
{ int i; for (i=0; i < (N); ++i) (A)[i] = (B)[i]; }
设计时不考虑操作的数据类型的编程方法是一种泛型编程方法。使用define宏就是泛型编程的一种形式。使用宏的优点是简单,为人熟知并且高效,其中为人熟知是因为在C程序设计中长期使用宏,而高效是因为不需要函数调用。
使用宏的缺点包括类型安全性、不可预期性和作用域问题。使用define宏通常可以运行,但是不能保证类型安全性;宏替换是由预处理器进行的文本替换,替换时并不会进行语法检查;define宏的另一个问题是可能导致对单个参数重复求值:宏的定义依赖于它们在文件中的位置,而不是依赖于C++语言的作用域规则。代码
#define CUBE(X) ((X)*(X)*(X))
的作用与下列代码不同:
template<class T> T cube (T x) { return x * x * x;}
当调用cube(sqrt(7))时,函数sqrt(7)只调用一次,而使用CUBE define宏时要调用三次。
当表达式中类型混杂而且不适于进行类型转换时,使用模板比较安全。
文件 copy1.cpp
template<class TYPE>
void copy(TYPE a[], TYPE b[], int n)
{
for (int i = 0; i < n; ++i)
a[i] = b[i];
}
调用带特殊参数的copy()函数会使编译器生成基于这些参数的函数。如果不能生成正确的函数,会产生编译时错误。试回答下列调用将产生什么结果。
文件 copy1.cpp
double f1[50], f2[50];
char c1[25], c2[50];
int i1[75], i2[75];
char* ptr1 = c1, *ptr2 = c2;
copy(f1, f2, 50);
copy(c1, c2, 10);
copy(i1, i2, 40);
copy(ptr1, ptr2, 15);
copy(i1, f2, 50);
copy(ptr1, f2, 50);
最后两个copy()调用不能编译是因为参数类型与模板类型不匹配,这叫作归一错误(unification error),即参数类型与模板类型不匹配错误。下一节将讨论编译器如何实现参数匹配。如果将f2强制类型转换为
copy(i1, static_cast<int* >(f2), 50);
那么编译器能够进行编译,但结果通常都不正确。与上述处理相反,这里需要能够接受两种不同类型参数的泛型复制过程。
文件 copy2.cpp
template<class T1, class T2>
void copy(T1 a[], T2 b[], int n)
{
for (int i = 0; i < n; ++i)
a[i] = b[i];
}
这种形式能够逐个元素进行转换,这种转换通常更恰当,也更安全。
瞧,任何形状的饼干都可以!
6.2.1 签名匹配与重载
泛型例程经常不能工作在一些特殊情况下。例如,下面的交换模板可以作用于基本类型,例如int或char类型,但是不能作用于int或char类型的数组。为了使模板函数能够为特殊类型提供服务,需要为这些类型定义所有的操作。如果没有这个前提条件,这些类型的代码会编译失败。即使在模板可以编译时,为这些类型产生的代码也必须进行修正。下面的交换模板作用于基本类型:
文件 swap.cpp
// Generic swap
template <class T>
void swap(T& x, T& y)
{
T temp;
temp = x;
x = y;
y = temp;
}
在参数完全匹配,不会产生二义性时,函数模板会生成适当的函数。
int i, j;
char str1[100], str2[100], ch;
complex c1, c2;
char *s1 = str1, *s2 = str2;
swap(i, j); // i j int - okay
swap(c1, c2); // c1, c2 complex - okay
swap(str1[50], str2[33]); // both char variables-okay
swap(i, ch); // i int ch char - illegal
swap(str1, str2); // illegal
swap(s1, s2); // legal- but may not be intention
在前三种情况下,模板能够按照预期进行编译和运行。但因为两个参数类型不同,swap(i, ch)将产生语法错误。swap(str1, str2)中,str1和str2是数组名,它们是不可改变的指针值。因此,不能编译代码x=y;。最后一种情况是swap(s1, s2),两个参数类型相同并且可以修改,所以函数swap(s1, s2)可以编译并执行,但是这里没有复制指针所指向的数组内容,而只是交换了指针本身。
为了让swap()作用于表示成字符数组的字符串,可以编写下列代码:
void swap(char* s1, char* s2)
{
int max_len;
max_len = (strlen(s1) >= strlen(s2)) ?
strlen(s1) : strlen(s2);
char* temp = new char[max_len + 1];
strcpy(temp, s1);
strcpy(s1, s2);
strcpy(s2, temp);
}
swap()函数的特殊版本交换了由指针值表示的两个字符串。加上这一特殊版本后,调用swap()函数时,函数签名的精确匹配的非模板版本将优先于精确匹配的模板版本。swap()函数的这个特殊版本是一个危险的交换例程,原因是短字符串分配的内存较少,交换时长字符串可能会溢出。在有多个重载函数时,使用下面的重载函数选择算法决定调用哪一个函数。
重载函数选择算法
1. 使用非模板函数进行精确匹配(可能带有微小转换)。
2. 使用函数模板进行精确匹配。
3. 对非模板函数使用普通参数解析。
6.2.2 如何编写简单函数square()
复习一下关于简单函数的一些内容,我们选择函数square()作为测试用例。
// Hand-coding genericity by overloading the function
inline int square(int n)
{
return n * n;
}
inline double square(double x)
{
return x * x;
}
这里使用文本编辑器复制基本的代码并且按照需要修改相应的类型,这样函数square()就可以重载为多种签名。但是这种方法的缺点是需要手工编写代码,而在手工复制和改变类型时,容易产生错误。
// Macro square
#define SQUARE(X) ((X)*(X))
这里使用一个预处理程序在需要的地方对替换代码进行内联处理,这种处理与类型无关。但是,因为宏使用文本替换而不是检查语言规则,因此也容易产生错误。注意,编译器对语法错误的检查是在文本替换之后进行的。
// Poor attempt at genericity using void*
inline double square(void* p)
{
double* temp = reinterpret_cast<double*>(p);
return (*temp) * (*temp);
}
这里使用void*作为参数类型,但是因为要使用与系统相关的强制类型转换,所以也容易出现错误。因此,虽然在将void*强制类型转换为double类型后代码可以工作,但这并不是合适的处理方式。
模板能够解决前面提到的所有问题:
// C++ template
template <class T>
inline T square(T n)
{
return n * n;
}
根据对简单的基本类型的测试,可以很容易地完成上述代码。在定义了乘法运算符operator*后,上述代码就成为通用代码,必要时可以生成任意的签名。为了测试读者对上述问题的理解情况,请完成本章的习题4。







