假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未经加工的文字。
若编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于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
class MsgInfo{/*...*/ };//这个class用来保存信息,以备将来产生信息
template<typename Company>
class MsgSender {
public:
//...//构造函数、析构函数等等
void sendClear(const MsgInfo& info)
{
std::string msg;
//...//根据info产生信息
Company c;
c.sendCleartext(msg);
}
//类似sendClear,唯一不同的是这里调用c.sendEncrypted
void sendSecret(const MsgInfo& info){/*...*/ }
};
这个做法行得通。但假设我们有时候想要在每次送出信息时志记某些信息。
derived class可轻易加上这样的生产力,那似乎是个合情合理的解法:
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
//...//构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
//...//将“传送前”的信息写至log;
sendClear(info);//调用base class函数,这段代码无法通过编译。
//...//将“传送后”的信息写至log;
}
//...
};
对于严守规律的编译器而言,上述代码无法通过编译,编译器会抱怨sendClear不存在。为什么?
问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然,它继承的是MsgSender<Company>,但其中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而若不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么,更明确地说是没办法知道它是否有个sendClear函数。
为了让问题更具现化,假设我们有个class CompanyZ坚持使用加密通讯:
//这个class不提供sendCleartext函数
class CompanyZ {
public:
//...
void sendEncrypted(const std::string& msg);
//...
};
一般性的MsgSender template对CompanyZ并不合适,因为那个template提供了一个sendClear函数(其中针对其类型参数Company调用了sendCleartext函数),而这对CompanyZ对象并不合理。
欲矫正这个问题,我们可以针对CompanyZ产生一个MsgSender特化版:
//一个全特化的MsgSender,
//它和一般template相同,
//差别只在于它删掉了sendClear。
template<>
class MsgSender<CompanyZ> {
public:
//...
void sendSecret(const MsgInfo& info){/*...*/ }
//...
};
上述代码最前面的"template<>"语法象征这既不是template也不是标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用。
这是所谓的模板全特化:template MsgSender针对类型CompanyZ特化了,而且其特化是全面性的,也就是说,一旦类型参数被定义为CompanyZ,再没有其他template参数可供变化。
现在,再次考虑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 template有可能被转化,而那个特化版本可能不提供和一般性template相同的接口。因此,它往往拒绝在templatized base class(模板化基类,本例的MsgSender<Company>)内寻找继承而来的名称(本例的SendClear)。
为了重头来过,我们必须有某种办法令C++“不进入templatized base class观察”的行为失效。
有三个办法。
第一是在base class函数调用动作之前加上"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:
//告诉编译器,请它假设sendClear位于base class内
using MsgSender<Company>::sendClear;
//...
void sendClearMsg(const MsgInfo& info)
{
//...//将“传送前”的信息写至log;
sendClear(info);//假设sendClear将被继承下来
//...//将“传送后”的信息写至log;
}
//...
};
在这里,编译器不进入base class作用域内查找,于是通过using告诉它,请它那么做。
第三是明白指出被调用的函数位于base class内:
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
//...
void sendClearMsg(const MsgInfo& info)
{
//...//将“传送前”的信息写至log;
MSgSender<Company>::sendClear(info);//假设sendClear将被继承下来
//...//将“传送后”的信息写至log;
}
//...
};
但这往往是最不让人满意的一个解法,因为若被调用的是virtual函数,上述的明确资格修饰会关闭“virtual绑定行为”。
从名称可视点的角度出发,上述每一个解法做的事情都相同:对编译器承诺“base class template的任何特化版本都将支持其一般版本所提供的接口”。这样一个承诺是编译器在解析像LoggingMsgSender这样的derived class template时需要的。但若这个承诺最终未被实践出来,往后的编译最终还是会还给事实一个公道。
例如:
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
//...//在msgData内放置信息
zMsgSender.sendClearMsg(msgData);//错误,无法通过编译
其中对sendClearMsg的调用动作将无法通过编译,因为在那个点上,编译器知道base class是个template特化版本MsgSender<CompanyZ>,而且它们知道那个class不提供sendClear函数,而后者却是sendClearMsg尝试调用的函数。
总结
可在derived class template内通过"this->"指涉base class template内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。