条款43 学习处理模板化基类内的名称
条款44 将与参数无关的代码抽离templates
背景是这样的,有两个不同的公司,然后想设计一个MessageSender,为这两个公司发送不同的消息,既支持明文发送SendClearText,也支持密文发送SendEncryptedText。一种思路是采用动态绑定的方法,定义一个BasicMessageSender,里面有两个方法,分别是发送明文和密文的虚函数,然后定义它的子类MessageSenderForCompanyA,以及MessageSenderForCompanyB,在这两个子类里面覆盖发送明文和密文的虚函数,从而达到根据不同公司发送不同消息的目的。
但这里我们想换一种思路,使用静态多态的方法来实现,静态多态就是模板的技术了,代码如下:
class CompanyA
{
public:
void SendClearText(){}
void SendEncryptedText(){}
};
class CompanyB
{
public:
void SendClearText(){}
void SendEncrypedText(){}
};
template
class MsgSender
{
public:
void SendClearText(){}
};
template
class MsgSenderWithLog: public MsgSender
{
public:
void SendClearTextWithLog()
{
// Logs
SendClearText(); // 有的编译器会编不过
}
};
int main()
{
MsgSenderWithLog MsgSender;
MsgSender.SendClearTextWithLog();
}
CompanyA与CompanyB有各自的发送函数,然后有一个模板类MsgSender,这个模板待确定的参数是T,可以是CompanyA或者CompanyB,这样就可以在定义MsgSender时,比如MsgSender或者MsgSender,指定到底调用的哪个公司的发送函数了,这是在编译期就可以确定下来的事情。
但现在有一个新问题,那就是我们希望在执行发送函数之前,还是加上日志比较好,这样我们就继承了MsgSender,定义了一个新类MsgSenderWithLog,在这里定义了一个新的函数SendClearTextWithLog,在这个函数里面调用了父类的SendClearText。
对于这段代码,其实思路还是挺清晰的,但问题是有的编译器会编不过这行代码(VS2008以后的版本的都是可以的,之前的版本没试),为什么?
这是因为在模板技术中存在全特化的概念,比如C公司,这个公司根本不想发送明文,也就是说它只有SendEncryptedText()接口,没有SendClearText()。为了使我们的静态多态仍然可用,我们这样定义只适用于C公司的MsgSender:
class CompanyC
{
public:
void SendEncryptedText(){}
;
template <>
class MsgSender
{
public:
void SendEncryptedText(){}
};
这时候如果去调用:
MsgSenderWithLog MsgSenderC;
MsgSenderC.SendClearTextWithLog(); //编译器
无法通过编译
这样编译器会报找到SendClearText()的错。正是因为有的编译器考虑到了全特化模板版本可以与普通版本不同,所以在有继承关系存在时,对直接调用父类的函数给出了不支持的error。但这个error是与编译器相关的,不是必然出现的。
为了让更多的编译器放弃这种全特化的忧虑,书上提供了三种解决方法:
方法一:
void SendClearTextWithLog()
{
// Logs
SendClearText(); // 有的编译器会编不过这段代码
}
改成
void SendClearTextWithLog()
{
// Logs
this->SendClearText(); // 这下能编译通过了
}
方法二:
在子类中声明using MsgSender::SendClearText;
编译器报error本质是不进行模板父类域的查找,所以这里using了父类的一个函数名,强制编译器对之进行查找。
方法三:
将SendClearText()指明为MsgSender::SendClearText()。
可在derived class template内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
“将与参数无关的代码抽离template”,这里的参数既可以指类型,也可以是非类型,我们先来看看非类型的情况。
假定我们要为矩阵写一个类,这个矩阵的行列元素个数相等,是一个方阵,因而我们可以对之求逆运算。因为方阵的元素可以有多种类型,同时方阵的维数(方阵大小)也可以不同,像下面这样,我们使用了模板:
template <class T, size_t n>
class SquareMatrix
{
public:
void Invert();
};
int main()
{
SquareMatrix<int, 10> a;
SquareMatrix<int, 5> b;
}
模板既可以指定类型,也可以指定其他参量,比如这里的size_t,用来表示方阵的维数。程序看起来是没有问题的,编译也是通过的,但从代码优化上看,还是有空间可以做的。a和b虽然都是元素为int型的方阵,但因为方阵维数不同,因而生成了两个Invert函数,这两个Invert函数的代码只是在循环个数上不同(取觉于方阵维数n),但算法思想完全是一样的。
为了防止编译器生成冗余的代码,我们可以将Invert抽离这里,变成一个以n为参数的函数,像下面这样:
class SquareMatrixBase
{
public:
SquareMatrixBase(T* p) : DataPointer(p){}
void Invert(size_t n){}
private:
T* DataPointer;
};
template <class T, size_t n>
class SquareMatrix: private SquareMatrixBase<T>
{
public:
SquareMatrix() : SquareMatrixBase(Data)
{}
void Invert()
{
SquareMatrixBase::Invert(n);
}
private:
T Data[n * n];
};
这里我们定义了一个BaseSquareMatrix类,这个类可以视为一个工具类,因为SquareMatrix是private继承于它的,我们把算法一致、只与维数n相关的算法函数都放在BaseSquareMatrix中,且将n作为它的Invert()函数的形参,为了使BaseSquareMatrix可以访问数据,从而求逆运算,我们声明了它的成员指针,这个指针将会在构造时指向子类的数据存储空间。
子类SquareMatrix没有大变化,只是在Invert的时候,用了一句代码就搞定了——调用父类的Invert函数,并把维数n传过去。
这样在main函数中,针对类型都是int,但矩阵维数不同的情况,Invert中生成的冗余代码非常少(只有一句话),父类也因为模板参数与维数无关,所以也只生成一份。从这个角度来看,将与参数无关的代码抽离template起到了减少代码冗余度的优化作用。
目前只是说将与类型无关的代码进行抽离(造一个父类,把算法相同的部分提取出来,放到父类),对于不同类型,其实也可以通过void*来实。比如STL,要想令我们使用list