条款 43:学习处理模板化基类内的名称

《Effective C++ 中文版 第三版》读书笔记

条款 43:学习处理模板化基类内的名称

我们需要一个程序,传送信息到不同的公司去。信息要不译成密码,要不就是未加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至那一家公司,就可以采用基于 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 
class MsgSender{ 
public: 
    void sendClear(const MsgInfo& info) 
    { 
        std::string msg; 
        根据info产生信息; 
        Company c; 
        c.sendCleartext(msg); 
    } 

    void sendSecret(const MsgInfo& info) 
    { 
        ...;//调用c.sendEncrypted,类似sendClear 
    } 
};

这个做法行的通。但假设我们有时候想要在每次发送出信息的时候志记(log)某些信息。 derived class 可以轻易加上这样的行为,那似乎是个合情理的解法:

template 
class LoggingMsgSender: public MsgSender{ 
public: 
    void sendClearMsg(const MsgInfo& info) 
    { 
        将传送前信息写至log; 
        sendClear(info); 
    } 
};

sendClearMsg 避免遮掩 “继承而得的名称”(条款 33),避免重新定义一个继承而得的 non-virtual 函数(条款 36)。但上述代码无法通过编译,编译器看不到 sendClear。为什么?

问题在于,编译器遇到 class template LoggingMsgSender 定义式时,并不知道它继承什么样的 class。因为 MsgSender 中的 Company 是个 template 参数,不到后来(当 LoggingMsgSender 被具现化)无法确切知道它是什么。而如果不知道 Company 是什么,就无法知道 class MsgSender 看起来是个什么样 —— 更明确的说是没办法知道它是否有个 sendClear 函数。

为了让问题具体化,假设有个 class CompanyZ 只是用加密通信:

class CompanyZ { 
public: 
    void sendEncrypted(const std::string& msg); 
};

一般性的 MsgSender template 对 CompanyZ 并不合适,因为那个 template 提供了一个 sendClear 函数(其中针对其类型参数 Company 调用了 sendCleartext 函数),而这对 CompanyZ 对象并不合理。与纠正这个问题,我们可以针对 CompanyZ 产生一个 MsgSender 特化版;

template<>                                            //一个全特化的 
class MsgSender{                // MsgSender;它和一般 template 相同 
public:                                                    //差别只在于它删掉了 sendClear 
    void sendSecret(const MsgInfo& info) 
    { 
        ... 
    } 
};

注意 class 定义式最前头 “template<>” 语法象征这既不是 template 也不是标准 class,而是个特化版的 MsgSender template,在 template 实参是 CompanyZ 时被使用。这事模板全特化(total template specialization):template MsgSender 针对类型 CompanyZ 特化了,而且其特化是全面性的,也就是说一旦类型参数被定为 CompanyZ,再没有其他 template 参数可供变化。

template 
class LoggingMsgSender: public MsgSender{ 
public: 
    void sendClearMsg(const MsgInfo& info) 
    { 
        将传送前信息写至log; 
        sendClear(info);// 如果 Company==CompanyZ,这个函数就不存在 
    } 
};

那就是为什么 C++ 拒绝这个调用的原因:它知道 base class template 可能被特化,而那个特化版本可能不提供和一般属性 template 相同的接口。因此它往往拒绝在 templatized base class(模板化基类,MsgSender)内寻找继承而来的名称(本例的 SendClear)。从 Object Oriented C++ 跨进 Template C++ 继承就不想以前那般畅通无阻了。

我们必须令 C++ “进入 templatized base classes 观察”。有三个办法:

第一个办法是 base class 函数调用动作之前加上 “this->”:

template 
class LoggingMsgSender: public MsgSender{ 
public: 
    void sendClearMsg(const MsgInfo& info) 
    { 
        将传送前信息写至log; 
        this->sendClear(info); //成立,假设sendClear将被继承 
    } 
};

第二个办法是使用 using 声明式:

template 
class LoggingMsgSender: public MsgSender{ 
public: 
    using MsgSender::sendClear;    // 告诉编译器,请他假设 sendClear 位于 base class 内 
    void sendClearMsg(const MsgInfo& info) 
    { 
        将传送前信息写至log; 
        sendClear(info); //成立,假设sendClear将被继承 
    } 
};

这里的 using 声明式不是条款 33 中 “base class 名称被 derived class 名称遮掩”,而是编译器不进人 base class 作用域查找,于是我们通过 using 告诉它,请他这么做。

第三个做法是,明白指出被调用的函数位于 base class 内:

template 
class LoggingMsgSender: public MsgSender{ 
public: 
    void sendClearMsg(const MsgInfo& info) 
    { 
        将传送前信息写至log; 
        MsgSender::sendClear(info); //成立,假设sendClear将被继承 
    } 
};

但这往往不是令人满意的一个解法,因为如果被调用的是 virtual 函数,上述的明确资格修饰 MsgSender:: 会关闭 virtual 绑定行为。

从名称可视点的角度出发,上述每个解法做的事情都相同:对编译器承诺 “base class template 的任何特化版本都将支持其一般化版本所提供的接口”。这样一个承诺是编译器在解析(parse)像 LoggingMsgSender 这样的 derived class template 时需要的。但如果这个承诺最终未被实践出来,往后的编译器最终还是会给事实一个公道。例如,如果稍后的源码内含这个:

LoggingMsgSender zMsgSender; 
MsgInfo msgData; 
zMsgSender.sendClearMsg(msgData);        // 错误!无法通过编译。

因为在那个点上,编译器知道 base class 是个 template 特化版本 MsgSender,而它们知道那个 class 不提供 sendClear 函数,而这个函数却是 sendClearMsg 尝试调用的函数。

根本而言,面对 “指涉 base class members” 之无效的 references,编译器的诊断时间可能发生在早期(当解析 derived class template 的定义式时),也可能发生在晚期(当那些 templates 被特定之 template 实参具现化时)。C++ 的政策是宁愿早诊断。这就是为什么 “当 base classes 从 templates 中被具现化时” 它假设它对那 base classes 的内容毫无所悉的缘故。

请记住:
可在 derived class template 内通过 “this->” 指涉 base class template 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成。

你可能感兴趣的:(条款 43:学习处理模板化基类内的名称)