C++之基于对象编程的支持II
Support for Object-Based Programming II
对基于对象编程范式支持
前面章节引入C++提供的语法来支持基于对象编程,已经展示了如何实现一个类型vector来储存整数。但是,为什么要只定义一个整型vector?可能用户会使用某种类型元素的vector,而vector编写者可能并不知晓这种类型。自然vector类型应该以这样的方式表达出来:它可以把元素类型作为一个参数。本章的主题就是展示如何用C++实作这些。
III-1 Parameterized types 参数化类型
透过关键字template引入参数化类型到C++里。考虑vector定义:
// declare a vector of elements of type T
template class vector
{
private:
int sz;
T *data;
public:
vector(int s)
{
if (s <= 0) error("bad vector size");
data = new T[sz = s];
}
vector(const vector & v);
// ...
T & operator[] (int i);
int size() const { return sz; }
// ...
};
// initialize a template vector from another vector
template vector::vector(const vector & a)
{
sz = a.sz; // same size
data = new T[sz]; // allocate element array
for (int i = 0; i < sz; i++) // and copy elements
data[i] = a.data[i];
}
是不是很简单?确实如此:)。而且一个简单的class指定一个类型,template透过指定template的参数产生一族类型(本例中是类型T)。人们需要做的只是改变下符号(与前面整型vector定义相比较)。语义的不同如下:
1.参数化的类由关键字template以及随后的一系列参数 引入。
2.我们创建新类型由vector指定。
3.在类外面的实现必须由前缀template 。
III-1.1 Template instantiation 模板的实例化
有了上面的定义,特定类型的vector可以定义使用如下:
vector v1(100); // v1 is a vector of 100 integers
vector v2(200); // v2 is a vector of 200 rational numbers
v2[i] = rational(v1[i], v1[i]);
...
因为编译器用实际的类型替换了参数T创建了一个新的类型,所以上面的用法是正确的。因此我们的例子里,编译器透过分别用int和rational替换T创建了vector以及vector两个类型。
Code duplication
不幸地是因为在实例化时创建的类型彼此之间互不相关,结果生成的代码可能包含两个版本的相同函数。在我们列举的这个例子中,对于成员函数size有两个版本(一种类型一个),尽管函数没有依赖于参数T。但这产生的影响我们称之为代码重复(code duplication)可以认为是参数化类型的一个问题。然而这也正是参数化类型效率之所在(一般实现良好的编译器,使用模板不会增加运行时开销)。
III-2 Parameterized functions 参数化函数
和类型相似,参数化也能用于产生函数族。同样和vector一样的方式,我们实现了一个swap函数:
template void swap(T & a, T & b)
{
T tmp(a); a = b; b = tmp;
}
实际对a、b的引用可以是int或double或者其他类型。Swap这么使用:
int x = 12, y = 15;
swap(x, y);
double a = 12, b = 15;
swap(a, b);
一旦编译器碰到使用swap的地方,它会产生分散的代码:swap(int, int) 和swap(double, double)。这些函数不会产生名字冲突的原因是因为他们的名字被改变为包含它们参数类型(这项技术称之为mangling,也用于运算符重载保证名字唯一)。
III-3 Parameterized functions vs. macros
很自然有个问题:模板与宏机制为什么不同呢?例如,前面swap函数可以很容易改写成这样 :
#define swap2(type,a,b) { type tmp = (a); (a) = (b); (b) = tmp; }
这似乎看起来用预处理器define关键字提供了一个安全的解决方案。现在让我们以错误的输入调用这两个版本:
int x = 2;
swap(1, x); // line 1
swap2(int, 1, x); // line 2
GNU C++编译器产生如下错误信息:
[我机器上的编译错误:
/home/lee/app_dev/cpp_ds_algo/cpp_ds_algo_lernu/template_example.cpp||In function ‘int main(int, char**)’:|
/home/lee/app_dev/cpp_ds_algo/cpp_ds_algo_lernu/template_example.cpp|72|错误: 对‘swap(int, int&)’的调用没有匹配的函数|
/home/lee/app_dev/cpp_ds_algo/cpp_ds_algo_lernu/template_example.cpp|60|附注: 备选为: void swap(T&, T&) [with T = int]|
/usr/include/c++/4.3/bits/stl_move.h|80|附注: void std::swap(_Tp&, _Tp&) [with _Tp = int]|
/home/lee/app_dev/cpp_ds_algo/cpp_ds_algo_lernu/template_example.cpp|73|错误: 赋值运算的左操作数必须是左值|
||=== Build finished: 4 errors, 0 warnings ===|
]
看到诧异了吧?第一个case,编译器检查swap参数的类型,发现int不是int&。跌第二个case编译器将swap2宏展开如下:
{ int tmp = (1); (1) = (x); (x) = tmp; }
然后最后才发现(1) = (x);赋值运算无法完成,因为1是常量。所以看来模板提供了更好的类型检查。
III-3.1 Inlining
还有一个方面需要考虑的那就是速度。宏产生和手写一样快速代码,因为它们只是扩展为实际的代码。似乎看出来使用模板意味着效率丢失,但事实并非如此。在C++里,一个函数可以声明为inline:这关键字指示编译器在类型检查后扩展函数代码(如果函数太大,编译器可能选择不inline函数,避免代码的膨胀)。继续考虑swap这个例子:
template
inline void swap(T & a, T & b)
{
T tmp(a); a = b; b = tmp;
}
这个版本的swap和函数一样安全,与宏一样快速。在class声明内部实现的成员函数被看作inline。在class声明外面实现的成员函数必须显式加inline才是inline函数。
III-4 Template specialization 模板特制化
模板提供了强大机制能产生函数或者类型族。然而在某些情形下,需要使用特定类型当作参数用于模板,需要特别的处理。例如,因为四舍五入、0检查等,一个计算整数矩阵的determinant算法和计算实数矩阵的determinant的算法是不同的,C++允许编写特制化的类或者特制模板类的某个特定的函数。后面的例子阐述了做法。首先从一个模板类的声明开始:
template class matrix
{
public:
matrix() { }
洃愀琀爀椀砀() { }
T det() const;
};
最能特制化的就是为特定类型重写一般类。这是我们在用一个特制的类型替代通用一般类时的做法,导致重写模板类型的大部分:
class matrix
{
public:
matrix();
洃愀琀爀椀砀();
int det() const;
};
matrix::matrix() {}
matrix:: 洃愀琀爀椀砀() {}
int matrix::det() const
{
cout << "matrix::det() called" << endl;
return 0;
}
然而,通常情况是当我们用一个特定的类型重写一个成员函数的一般实现:
double matrix::det() const
{
cout << "matrix::det() called" << endl;
return 0.0;
}
最后,再对其他所有类型提供一般通用的实现
template
T matrix::det() const
{
cout << "matrix::det() called" << endl;
return T();
}
有了这些定义实现,用户可以这样使用matrices:
matrix mi;
matrix md;
matrix mc;
mi.det();
md.det();
mc.det();
程序输出如你所愿:
matrix::det() called
matrix::det() called
matrix::det() called
保持良好的编程风格,首先声明一般类,然后声明和实现特制化部分,最后实现一般算法。如果不按这顺序,旧的C++编译器可能会出问题。
[特制化时是否只重写某几个成员函数还是重写这个类?文章里面作者似乎没有说清楚,但试验下来,还是后者!!]
III-5 Exercises
重写complex类,用带一个参数T的模板类做complex算术运算,对float特制化complex数字
- 评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)
-
