模板与泛型编程
本节两个概念即什么是隐式接口,什么是编译期多态。
面向对象编程世界总是以显示接口(expicit interfaces)和运行期多态(runtime polymorphism)解决问题。但是在templates 及泛型编程的世界,隐式接口和编译期多态更重要。
隐式接口是相对于函数签名(也就是函数名称、参数类型、返回类型)所代表的显式接口而言的。当我们看到一个函数签名,比如说:
string GetNameByStudentID(int StudentID);
我们就知道这个函数有一个整型的形参,返回值是string。
但隐式接口是由有效表达式组成的,考虑一个模板函数,像下面这样:
1 template
2 void TemplateFunction(T& w)
3 {
4 if(w.size() > 10){…}
5 }
T可以是int,可以double,也可以是自定义的类型。光看这个函数声明,我们不能确定T具体是什么,但我们知道,要想通过编译,T必须要支持size()这个函数。也就是说,T中一定要有这样的函数接口声明。
ReturnValue size();
当然返回值ReturnValue不一定是int了,只要它能支持operator > (ReturnValue, 10)这样的运算即可。这种由表达式推判出来的函数接口,称之为隐式接口。
简言之,显式接口由函数签名式构成,隐式接口由有效的表达式组成。
至于编译期多态,我们在讨论继承时就已经多次提到“运行时多态”了,它伴随着virtual关键字,本质是一个虚表和虚指针,在类对象构造时,将虚指针指向了特定的虚表,然后运行时就会根据虚表的内容进行函数调用。
“编译期多态”,从字面上来看,它发生在编译阶段,实际上就是template 这个T的替换,它可以被特化为int,或者double,或者用户自定义类型,这一切在编译期就可以决定下来T到底是什么,编译器会自动生成相应的代码(把T换成具体的类型),这就是编译期多态做的事情了,它的本质可以理解成自动生成特化类型版本,T可以被替换成不同的类型,类似于函数重载,比如同时存在int版本的swap与double版本的swap,形成函数重载。
运行时多态是决定“哪一个virtual函数应该被绑定”,虚函数在子类继承中的多态性。
记住:
class和templates都支持接口与多态;
对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;
对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
问题引入:以下template声明中,class和typename有什么不同?
template <class T> class Widget;
template <typename T> class Widget;
答案:没有不同。当我们声明typename类型参数。class和typename的意义完全相同。
假设我们要打印一个容器(里面为)中的第二个元素,那么函数应该是这样:
template <typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout<<*iter;
C::const_iterator *x; //容易引起歧义
}
}
iter local变量,类型是C::const_iterator,实际是什么取决于template参数C。 template内出现的名称如果依赖于某个template参数,就称作从属名称。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。
value类型是int 。int 是一个并不依赖于任何template 参数的名称,叫做非从属名称(non-dependent names)。
嵌套从属名称容易导致解析困难。比如上例中 C::const_iterator x ; 看起来x是一个local变量,但是也可能会被误认为C:: const_iterator 是个类型变量 是个相乘操作。所以需要告诉编译器 C::const_iterator 是个类型。在C::const_iterator 前面放置关键字 typename 即可。
总结:一般性规则,任何时候当你想在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字 typename。
但是总有例外:typename不能出现在基类列表的嵌套从属类型之前,也不可出现在成员初始化列表中作为类型的限定符。这样的例外并不容易操作。
template <typename T>
class Derived:public Based<T>::Nested{ //base class list 中不允许typename
public:
explicit Derived(int x)
:Based<T> ::Nested(x) //member ini.list 不允许typename
{
typename Base <T>:Nested temp; //嵌套从属类型不在两个例外之内,所以要加typename
...
}
...
};
记住:
◆当声明template参数时,typename与class可以互换
◆使用typename标识嵌套从属类型名称,但是在基类列表和成员初始哈列表中,不能使用typename来指明从属名称。
我们必须有某种办法令C++“不进入templatized base class观察”的行为失效。有一下三种办法
1、在base class函数调用动作之前加上“this->”
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
……//析构构造等
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
this->sendClear(info);
//传送后信息写到log
}
};
2、使用using声明式,有点类似**条款**33。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
……//析构构造等
uinsg MsgSender<Company>::sendClear;//告诉编译器,假设sendClear位于base class内
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
sendClear(info);//可以编译通过,假设sendClear将被继承
//传送后信息写到log
}
};
补充一下,这里情况和**条款**33不同,这里不是将被掩盖的base class名称带入一个derived class作用域内,而是编译器不进入base class作用域内查找,通过using告诉编译器,请它去查找。
3、明白指出被调用的函数位于base class内
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
……//析构构造等
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
MsgSender<Company>::sendClear(info);//可以编译通过,假设sendClear被继承
//传送后信息写到log
}
};
这种做法使用了明确资格修饰符(explicit qualification),这将会关闭virtual绑定行为。
回头再看上面三种做法,原理相同:对编译器承诺“base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。这样的承诺是编译器在解析(parse)像LoggingMsgSender这样的derived class template时需要的。但如果这个承诺稍后没有兑现,编译器还会给事实一个公道。例如,稍后源码内:
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
zMsgSender.sendClearMsg(msgData);//错误,无法编译通过
在调用sendClearMsg这个点上,编译器直到base class是个template特化版本,且它直到这个特化版不提供sendClearMsg函数。
总结一下,本条款讨论的是,面对指涉base class member之无效references,编译器的诊断时间可能发生在早起(当解析derived class template定义时),也可能发生在晚期(当template被特定值template实参具体化时)。C++宁愿较早诊断,这也是为什么当base classes从template具体化时,它假设对那些base classes的内容毫无所悉的缘故。
记住:
可在derived class templates内通过this->指涉base class templates内的成员名称,或藉由一个明白写出base class资格修饰符完成。
Templates可以节省时间和避免代码重复。对于类似的classes或functions,可以写一个class template或function template,让编译器来做剩余的事。这样做,有时候会导致代码膨胀(code bloat):其二进制码带着重复(或几乎重复)的代码、数据,或者两者。但这时候源代码看起来可能很整齐。
先来学习一个名词:共性与变性分析(commonality and variability analysis)。比较容易理解。例如,你在编写几个函数,会用到相同作用的代码;这时候你往往将相同代码搬到一个新函数中,给其他几个函数调用。同理,如果编写某个class,其中某些部分和另外几个class相同,这时候你不会重复编写这些相同部分,只需把共同部分搬到新class中去即可,去使用继承或复合(**条款**32,38,39),让原先的classes取用这些共同特性,原classes的互异部分(变异部分)仍然留在原位置不动。
编写templates时,也要做相同分析,避免重复。non-template代码中重复十分明确:你可以看到两个函数或classes之间有所重复。但是在template代码中,重复是隐晦的,因为只有一份template源码。
例如,你打算在为尺寸固定的正方矩阵编写一个template,该矩阵有个支持逆矩阵运算的函数
template<typename T, std::size_t n>//T为数据类型,n为矩阵大小
class SquareMatrix{
public:
……
void invert();//求逆运算
};
SquareMatrix<double,5> sm1;
sm1.invert();//调用SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
sm2.invert();//调用SquareMatrix<double,10>::invert
上面会具体化两份invert。这两份函数几乎完全相同(除了一个操作5*5矩阵,一个操作10*10)。这就是代码膨胀的一个典型例子。
上面两个函数除了操作矩阵大小不同外,其他相同。这时可以为其建立一个带数值的函数,而不是重复代码。于是有了对SquareMatrix的第一份修改:
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();//编码遮掩base中的invert,**条款**33
public:
……
void invert()//求逆运算
{
this->invsert(n);//稍后解释为什么用this
}
};
SquareMatrixBase::invert只是企图避免derived classes代码重复,所以它以protected替换public。这个函数使用this->,因为模板化基类内的函数名称会被derived classes掩盖(条款**43)。注意,SquareMatrixBase和SquareMatrix之间继承关系是private,这说明base class是为了帮助derived classes实现,两者不是**is-a关系。
现在还有一个问题,SquareMatrixBase::invert操作的数据在哪?它在参数中直到矩阵大小,但是矩阵数据derived class才知道。derived class和base class如何联络?一个做法是可以为SquareMatrixBase::invert添加一个参数(例如一个指针)。这个行得通,但是考虑到其他因素(例如,SquareMatrixBase内还有其他函数,也要操作这些数据),可以把这个指针添加到SquareMatrixBase类中。
template<typename T>
class SquareMatrixBase{
protected:
SquareMatirxBase(std::size_t n,T* pMem)
:size(n), pData(pMem){}
void setDataPtr(T* ptr) {pData=ptr;}
……
private:
std::size_t size;
T* pData;
};
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<T>{
public:
SquareMatrix()
:SquareMatrixBase<T>(n, 0),
pData(new T[n*n])
{this->setDataPtr(pData.get());}
……
private:
boost::scoped_array<T> pData;
};
这样以来,类型相同的derived classes会共享base class。
记住:
◆ template 生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
◆ 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
◆ 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述(binary representation)的具现类型共享实现码。