假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于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);
...
};
... // 针对其他公司设计的classes
class MsgInfo { ... }; // 这个class用来保存信息,以备将来产生信息
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
};
这个做法行得通。但假设我们有时候想要在每次送出信息时记录(log)某些信息。derived class可轻易加上这样的生产力,那似乎是个合情合理的解法:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
... // 构造函数、析构函数等等。
void sendClearMsg(const MsgInfo& info)
{
// 将“传送前”的信息写至log;
sendClear(info); // 调用base class函数:这段代码无法通过编译
// 将“传送后”的信息写至log;
}
...
};
注意这个derived class的信息传送函数有一个不同的名称(sendClearMsg),与其base class内的名称(sendClear)不同。那是个好设计,因为它避免遮掩“继承而来的函数名称”(见条款33),也避免重新定义一个继承而得的non-virtual函数(见条款36)。但上述代码无法通过编译,至少对严守规律的编译器而言。这样的编译器会抱怨sendClear不存在。我们的眼睛可以看到sendClear的确在base class内,编译器却看不到它们。为什么?
问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么——更明确地说是没办法知道它是否有个sendClear函数。
为了让问题更具体化,假设我们有个class CompanyZ坚持使用加密通讯:
class CompanyZ { // 这个class不提供
public: // sendCleartext函数
...
void sendEncrypted(const std::string& msg);
...
};
一般性的MsgSender template对CompanyZ并不合适,因为那个template提供了一个sendClear函数(其中针对其类型参数Company调用了sendCleartext函数),而这对CompanyZ对象并不合理。欲矫正这个问题,我们可以针对CompanyZ产生一个MsgSender特化版:
template<> // 一个全特化的
class MsgSender<CompanyZ> { // MsgSender:它和一般template相同,
public: // 差别只在于它删掉了sendClear。
...
void sendSecret(const MsgInfo& info)
{ ... }
};
注意class定义式最前头的“template<>”语法象征这既不是template也不算标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用。这是所谓的模板全特化:template MsgSender针对类型CompanyZ特化了,而且其特化是全面性的,也就是说一旦类型参数被定义为CompanyZ,再没有其他template参数可供变化。
现在,MsgSender针对CompanyZ进行了全特化,让我们再次考虑derived class LoggingMsgSender:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
// 将“传送前”的信息写至log;
sendClear(info); // 如果Company == CompanyZ,这个函数不存在。
// 将“传送后”的信息写至log;
}
...
};
正如注释所言,当base class被指定为MsgSender<CompanyZ>时这段代码不合法,因为那个class并未提供sendClear函数!那就是为什么C++拒绝这个调用的原因:它指定base class templates有可能被特化,而那个特化版本可能不通过和一般性template相同的接口。因此它往往拒绝在templatized base classes(模板化基类,本例的msgSender<Company>)内寻找继承而来的名称(本例的SendClear)。就某种意义而言,当我们从Object Oriented C++跨进Template C++(见条款1),继承就不像以前那般畅行无阻了。
为了重头来过,我们必须有某种办法令C++“不进入templatized base classes观察”的行为失效。有三个办法,第一是在base class函数调用动作之前加上“this->”:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
// 将“传送前”的信息写至log;
this->sendClear(info); // 成立,假设sendClear将被继承。
// 将“传送后”的信息写至log;
}
...
};
第二是使用using声明式。如果你已读过条款33,这个解法应该令你感到熟悉。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear; // 告诉编译器,请它假设sendClear位于base class内。
...
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info); // 成立,假设sendClear将被继承。
...
}
...
};
第三个做法是,明白指出被调用的函数位于base class内:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
...
MsgSender<Company>::sendClear(info); // 成立,假设sendClear将被继承。
...
}
...
};
这个解法并不让人满意,因为如果被调用的virtual函数,上述的明确资格修饰会关闭“virtual绑定行为”。
请记住
- 可在derived class templates内通过“this->”指涉base class templates内的成员名称,或籍由一个明白写出的“base class资格修饰符”完成。