模板与泛型编程 Templates and Generic Programming
C++模板的最初设计目的是建立类型安全的容器如vector,list和map。后来发现模板有能力完成越来越多可能的变化。后面就出现了泛型编程,写出的代码和其所处理的对象类型彼此独立。STL算法如for_each,find和merge就是这类编程的成果。后面发现它可以被用来计算任何可计算的值。于是导出了模板元编程。创造出在C++编译器内执行并于编译完成时停止执行的程序。
41、了解隐式接口和编译期多态
Understand implicit interfaces and compile-time polymorphism.
面向对象编程世界总是以显式接口和运行期多态解决问题。举个例子,给定这样的class:
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
…
};
// 和这样的函数:
void doProcessing(Widget& w)
{
if(w.size() >10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
对于doProcessing内的w,由于w的类型被声明为Widget,所以w必须支持Widget接口,可以在源码中找到这个接口,这就为一个显式接口,由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态,也就是说将于运行期根据w的动态类型决定究竟调用哪个函数。
Templates及泛型编程中,和面向对象有根本上的不同。显式接口和运行期多态仍然存在,但重要性降低。隐式接口和编译期多态移到前面。如将上述的doProcessing从函数变成函数模板:
template<typename T>
void doProcessing(T& w)
{
if(w.size() >10 && w != someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
现在对于doProcessing内的w,w必须支持的接口由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数、copy构造函数(用来建立temp)、不等比较。重要的是这一组表达式便是T必须支持的一组隐式接口。凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功。这样这样的具现化发生在编译期。“以不同的template参数具现化”会导致调用不同的函数,这便是所谓的编译期多态。
通常显式接口由函数的签名式(也就是函数名称、参数名称、返回类型)构成。隐式接口则完全不同,它并不基于函数签名式,而是由有效表达式组成。如上面模板函数doProcessing,T的隐式接口有这些约束:必须提供一个名为size的成员函数,该函数返回一个整数值。必须支持一个operator!=函数,用来比较两个T对象等等。加诸于template参数身上的隐式接口,就像加诸于class对象身上的显式接口一样,而且两者都在编译期完成检查。就和无法以一种“与class提供的显式接口矛盾”的方式来使用对象(代码将通不过编译),也无法在template中使用“不支持template所要求之隐式接口”的对象(代码将通不过编译)。
- classes和templates都支持接口和多态。
- 对classes而言接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期。
- 对template参数参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
42、了解typename的双重意义
Understand the two meanings of typename.
很多时候typename和class是等价的,意义完全相同。但是也有例外。假设有个模板函数,接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints,然后打印其第二个元素值:
template<typename C>
void print2nd(const C& container) //打印容器内的第二元素
{ //这不是有效的C++代码
if(container.size() >= 2)
{
C::const_iterator iter(container.begin()); //取第一个元素的迭代器
++iter;
int value = *iter; //将该元素复制到某个int
std::cout << value;
}
}
template内出现的名称如果相依于某个template参数,称之为从属名称。如果从属名称在class内呈嵌套状,称为嵌套从属名称如C::const_iterator。实际它还是个嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。而变量value不依赖任何template参数,称为非从属名称。嵌套从属名称有可能导致解析困难如:
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
…
}
看起来好像声明x为一个local变量,是个指针,指向一个C::const_iterator。但是如果C::const_iterator不是一个类型且x是个全局变量名称,上述代码成了一个相乘动作。为了解除歧义,必须显式的声明其为类型,只需要给其前面加上typename即可:
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
…
}
}
Typename只被用来验明嵌套从属类型名称,其他名称不该有其存在。如下函数模板接受一个容器和一个指向该容器的迭代器:
template<typename C> //允许使用typename或class
void f(const C& container, //不允许使用typename
typename C::iterator iter); //一定要使用typename
上述的C并不是嵌套从属类型名称,所以声明container时并不需要以typename为前导,但C::iterator是个嵌套从属名称类型,所以必须以typename为前导。typename必须作为嵌套从属类型名称的前缀词的例外时typename不可以出现在基类list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为基类修饰符。例如:
template<typename T>
class Derived: public Base<T>::Nested{ //基类list中不允许typename
public:
explicit Derived(int x):Base<T>::Nested(x) //成员初值列不允许typename
{
typename Base<T>::Nested temp; //作为一个基类修饰符需加上typename
…
}
…
};
假设写一个函数模板,它接受一个迭代器,然后为该迭代器指涉的对象做一份local复件temp:
template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type tewmp(*iter);
…
}
std::iterator_traits::value_type是标准traits类的一种运用,相当于类型为IterT的对象所指之物的类型。这个语句声明一个local变量(temp),使用IterT对象所指物的相同类型,并将temp初始化为iter所指物。如果IterT是vector::iterator,temp的类型就是int。
- 声明template参数时,前缀关键字class和typename可互换。
- 使用关键字typename标识嵌套从属类型名称,但不得在基类列或成员初值列内以它作为基类修饰符。
43、学习处理模板化基类内的名称
Know how to access names in templatized base classes.
假设有个程序,能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未加工的文字。如果编译期间我们有足够信息来决定哪个信息传递哪个公司,用基于template实现:
class CompanyA{
public:
…
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
…
};
class CompanyB{
public:
…
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
…
};
…
class MsgInfo { … }; //保存信息,以备将来产生信息
template<typename Company>
class MsgSender{
public:
…
void sendClear(const MsgInfo& info)
{
std::string msg;
//根据info产生信息
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info) //类似sendClear,唯一不同调用c.sendEncrypted
{ … }
};
假设想在每次送出信息时记录日志。derived class实现如下:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
…
void sendClearMsg(const MsgInfo& info)
{
//将传送前的信息写至log
sendClear(info); //调用基类函数,代码是不能通过编译的。
//将传送后的信息写至log
}
};
这个派生类的信息传递函数有一个不同的名称sendClearMsg,与其基类内的名称sendClear不同,可以避免遮掩继承而得的名称,也避免重新定义一个继承而得的非虚函数。但上述代码不能通过编译,是因为编译器在处理class template LoggingMsgSender定义式时,并不知道它继承什么样的class。虽然看起来继承MsgSender,但其中的Company是个template参数,不到LoggingMsgSender被具现化无法确切知道它是什么。而且如果不知道Company是什么,就无法知道class MsgSender看起来像什么,更明确地说是没办法知道它是否有个sendClear函数。
为解决此问题,有三个办法:第一在基类函数调用动作之前加上this->:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
…
void sendClearMsg(const MsgInfo& info)
{
//将传送前的信息写至log
this->sendClear(info); //假设sendClear将被继承下来。
//将传送后的信息写至log
}
};
第二是使用using声明式:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear;
…
void sendClearMsg(const MsgInfo& info)
{
//将传送前的信息写至log
sendClear(info); //假设sendClear将被继承下来。
//将传送后的信息写至log
}
};
第三是明确指出被调用的函数位于基类内:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
…
void sendClearMsg(const MsgInfo& info)
{
//将传送前的信息写至log
MsgSender<Company>::sendClear(info); //假设sendClear将被继承下来。
//将传送后的信息写至log
}
};
第三个做法是最不可取的,因为如果被调用的是个virtual函数,上述的明确资格修饰会关闭virtual绑定行为。
- 可在派生类模板内通过this->指涉基类模板内的成员名称,或由一个明白写出的基类资格修饰符完成。
44、将与参数无关的代码抽离template
Factor parameter-independent code out of template.
Templates是节省时间和避免代码重复的一个奇方妙法。但是使用template可能会导致代码膨胀,其二进制码带着重复的代码、数据。一般编写某个class,然后发现和另一个class的某些部分相同,把共同的部分搬移到新类中,然后使用继承或复合令原先的类取用这些共同特性。编写template时,也是做相同的分析,以相同的方式避免重复,但在template代码中,代码重复时隐晦的,因为只存在一份template源码,需要自己感受当template被具现化多次时可能发生的重复。
假设为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。
template<typename T, std::size_t n> //template支持n*n矩阵,元素类型为T的对象。
class SquareMatrix{
public:
…
void invert(); //求逆矩阵
};
这个template接受一个类型参数T,除此之外还接受一个类型为size_t的参数,那是个非类型参数。这种参数和类型参数比起来较不常见,但完全合法。如下使用:
SquareMatrix<double, 5> sm1;
…
sm1.invert();
SquareMatrix<double, 10> sm2;
…
sm2.invert();
上述代码会具现化两份invert。这些函数并非完全相同,但是除了常量5和10,其它部分完全相同。这种现象是template造成代码膨胀。
对于此可以采用下述实现:
template<typename T > //与尺寸无关的基类
class SquareMatrixBase{
protected:
…
void invert(std::size_t matrixSize); //求逆矩阵
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>:: invert
public:
…
void invert(){ this->invert(n); }; //求逆矩阵
};
这种做法利用SquareMatrixBase:: invert使用protected避免代码重复。调用它而造成的额外成本是0,因为派生类的invert调用基类版本时用的是inline调用。但是这个需要每次告诉基类矩阵尺寸,需要额外添加一个额外参数,不够友好。所以可以让SquareMatrixBase储存一个指针,指向矩阵数值所在的内存,只有它存储了那些东西,也就可能存储矩阵尺寸:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() : SquareMatrixBase<T>(n,data) { }
…
private:
T data[n*n];
};
这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一个做饭是把每一个矩阵的数据放进heap:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase {
public:
SquareMatrix() : SquareMatrixBase<T>(n,0),pData(new T[n*n])
{ this->setDataPtr(pData.get()); }
…
private:
boost::scoped_array<T> pData;
};
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现化类型共享实现码。
上一篇: C++进阶_Effective_C++第三版(六) 继承与面向对象设计 Inheritance and Object-Oriented Design
下一篇: C++进阶_Effective_C++第三版(八) 定制new和delete Customizing new and delete