43:学习处理模块化基类内的名称

文章讨论了在C++中使用模板继承时遇到的问题,当基类模板有一个特化版本不包含特定函数时,派生类调用该函数会引发编译错误。解决方案包括使用`this->`前缀、using声明或明确的基类限定调用。这些方法允许编译器在派生类中找到基类模板的成员,即使它们在特化版本中不存在。
摘要由CSDN通过智能技术生成

假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未经加工的文字。

若编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于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资格修饰符”完成。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值