1. 问题的引入——派生类不会发现模板基类中的名字

假设我们需要写一个应用,使用它可以为不同的公司发送消息。消息可以以加密或者明文(未加密)的方式被发送。如果在编译阶段我们有足够的信息来确定哪个信息会被发送到哪个公司,我们可以使用基于模板的解决方案:

 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 for other companies class MsgInfo { ... }; // class for holding information
// used to create a message template<typename Company>
class MsgSender {
public:
... // ctors, dtor, etc. void sendClear(const MsgInfo& info)
{
std::string msg;
create msg from info;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info) // similar to sendClear, except { ... } // calls c.sendEncrypted }

这会工作的很好,但是假设有时候我们需要在发送信息之前log一些信息。一个派生类就能够很容易的添加这些信息,下面的实现看上去是合理的实现方式:

 template<typename Company>

 class LoggingMsgSender: public MsgSender<Company> {

 public:

 ...                                                         // ctors, dtor, etc.

 void sendClearMsg(const MsgInfo& info)     

 {                                                         

 write "before sending" info to the log;

 sendClear(info); // call base class function;
// this code will not compile!
write "after sending" info to the log;
}
...
};

注意派生类中的消息发送函数和基类相比(sendClear)是一个不同的名字(sendClearMsg)。这是好的设计,因为这避免了隐藏继承而来的名字的问题(Item 33),同时避免了重新定义继承而来的非虚函数问题Item 36)。但是代码不能通过编译,至少符合标准的编译器不能通过编译。这些编译器会发出sendClear不存在的抱怨。我们能够看到sendClear是在基类中,但是编译器没有在基类中发现它。我们需要知道为什么。

2. 问题分析

2.1 一般化分析

问题出现在当编译器遇到类模版LoggingMsgSender的定义时,它们不知道它继承自什么类。当然,它是继承自MsgSender<Company>,但是Company是一个模板参数,这个参数只有在LoggingMsgSender被实例化的时候才会被确认。在不知道Company是什么的情况下,我们也不知道MsgSender<Company>是什么样子的。因此也就没有方法获知是否存在sendClear函数。

2.2 用实例来证明问题所在

为了使问题更加具体,假设我们有一个类CompanyZ使用加密的方式进行通信:

 class CompanyZ {                                               // this class offers no

 public:                                                                // sendCleartext function

 ...                                                                       

 void sendEncrypted(const std::string& msg);   

 ...                                                                       

 };

普通的MsgSender模板对于CompanyZ来说是不合适的,因为普通模板提供了一个对于CompanyZ对象来说没有意义的函数。为了改正这个问题,我们能够为CompanyZ创建一个MsgSender的特化版本:

 template<>                                     // a total specialization of

 class MsgSender<CompanyZ> {      // MsgSender; the same as the

 public: // general template, except
... // sendClear is omitted
void sendSecret(const MsgInfo& info)
{ ... }

注意在类定义开始的地方出现的”template<>” 语法。它表明这既不是模板也不是单独的类。它是当使用CompanyZ作为模板参数时,会使用到的MsgSender模板的特化版本。这叫做模板全特化(total template specialization):模板MsgSender为类型CompanyZ进行了特化,并且特化是全特化——一旦类型参数被定义为ComanyZ,模板参数的其它地方就不会再发生变化

在MsgSender已经有了CompanyZ的特化版本的情况下,再看一下派生类LoggingMsgSender:

 template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
sendClear(info); // if Company == CompanyZ,
// this function doesn’t exist!
write "after sending" info to the log;
}
...
};

正如注释中所写的,当基类是MsgSender<CompanyZ>的情况下这段代码没有意义,因为基类中没有提供sendClear函数。这也是C++拒绝这个调用的原因:它认识到基类模板可能被特化了,但是特化版本并没有提供普通模板中的一般接口。因此,它拒绝在模板化基类中寻找继承而来的名字。从某种意义上讲,当我们从面向对象C++转到模板C++的时候(Item 1),继承就会停止工作。

3. 如何解决问题——三种方法

如果让其重新工作,我们必须让C++“不在模板化基类中寻找“的行为失效。有三种方法达到这个目标。

第一,调用基类函数时你可以为其加上”this->“

 template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
this->sendClear(info); // okay, assumes that
// sendClear will be inherited
write "after sending" info to the log;
}
...
};

第二,你可以使用using声明,你可能会熟悉,因为Item 33中用了类似的解决方案。那个条款中解释了如何使用using声明来将隐藏起来的基类名字带入派生类作用域。我们于是可以像下面这种方式实现sendClearMsg:

 template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear; // tell compilers to assume
... // that sendClear is in the
// base class
void sendClearMsg(const MsgInfo& info)
{
... sendClear(info); // okay, assumes that ... // sendClear will be inherited } ... };

最后,让你的代码通过编译的方法是在基类中明确指出需要调用的函数

 template<typename Company>

 class LoggingMsgSender: public MsgSender<Company> {

 public:

 ...

 void sendClearMsg(const MsgInfo& info)

 {

 ...

 MsgSender<Company>::sendClear(info);

 // okay, assumes that

 ... // sendClear will be
} // inherited
...
};

这基本上会是你最不愿意使用的解决这个问题的方法,因为如果被调用的函数是virtual的,显示的限定符会关掉virtual绑定行为。

从名字可见性的观点来看,每个方法都做了同样的事情:它向编译器许诺,接下来的任何基类模板特化都会支持一般模板提供的接口。这样的许诺是当所有的编译器解析一个像LoggingMsgSender的派生类模板的时候所需要的,但是如果这个许诺并没有兑现,在接下来的编译中真理就会浮现。例如,如果下面的源码有这种情况:

 LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
... // put info in msgData zMsgSender.sendClearMsg(msgData); // error! won’t compile

对sendClearMsg的编译不会通过,因为从这个点上来说,编译器知道基类是模板的特化版本MsgSender<CompanyZ>,并且它们知道这个类没有提供sendClearMsg想要调用的sendClear函数。

4. 本条款讨论的根本所在

从根本上来说,这个问题是编译器对基类成员的无效引用进行诊断的早(当派生类模板被解析的时候)或晚(当这些模板用特定的模板参数进行实例化的时候)的问题。C++的方针是更加喜欢早点诊断,这也是为什么当类从模板中特化的时候,它假定对基类的内容一无所知。

5. 总结

在派生类模板中,引用基类模板中的名字可以使用“->this“前缀,通过使用using声明,或者通过使用显示的使用基类限定符。

读书笔记 effective c++ Item 43 了解如何访问模板化基类中的名字的更多相关文章

  1. Effective C++ -----条款43:学习处理模板化基类内的名称

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

  2. 读书笔记 effective c++ Item 45 使用成员函数模板来接受“所有兼容类型”

    智能指针的行为像是指针,但是没有提供加的功能.例如,Item 13中解释了如何使用标准auto_ptr和tr1::shared_ptr指针在正确的时间自动删除堆上的资源.STL容器中的迭代器基本上都是 ...

  3. 读书笔记 effective C++ Item 33 避免隐藏继承而来的名字

    1. 普通作用域中的隐藏 名字实际上和继承没有关系.有关系的是作用域.我们都知道像下面的代码: int x; // global variable void someFunc() { double x ...

  4. 读书笔记 effective C++ Item 40 明智而谨慎的使用多继承

    1. 多继承的两个阵营 当我们谈论到多继承(MI)的时候,C++委员会被分为两个基本阵营.一个阵营相信如果单继承是好的C++性质,那么多继承肯定会更好.另外一个阵营则争辩道单继承诚然是好的,但多继承太 ...

  5. 读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来

    1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法.你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定 ...

  6. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

  7. 读书笔记 effective c++ Item 31 把文件之间的编译依赖降到最低

    1. 牵一发而动全身 现在开始进入你的C++程序,你对你的类实现做了一个很小的改动.注意,不是接口,只是实现:一个私有的stuff.然后你需要rebuild你的程序,计算着这个build应该几秒钟就足 ...

  8. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

  9. 读书笔记 effective c++ Item 35 考虑虚函数的替代者

    1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数, ...

随机推荐

  1. Dockerfile注意事项

    准则 尽量将Dockerfile放在空目录中,如果目录中必须有其他文件,则使用.dockerignore文件. 避免安装不必须的包. 每个容器应该只关注一个功能点. 最小化镜像的层数. 多行参数时应该 ...

  2. BZOJ 1085: [SCOI2005]骑士精神(A*算法)

    第一次写A*算法(这就是A*?如果这就是A*的话,那不就只是搜索的一个优化了= =,不过h函数如果弄难一点真的有些难设计) 其实就是判断t+h(x)(t为当前步数,h(x)为达到当前状态的最小步数) ...

  3. Oracle主键异常处理

    Hibernate: insert into test1.WarnWeather (WAREA, wdate, WDAYS, WINFO, WTYPE, WNO) values (?, ?, ?, ? ...

  4. 知识管理(KM) - 数据流

    快速链接: 人力资源知识体系索引 本章主要列出知识管理(KM)中涉及到的所有表. 步骤 操作 相关表 说明 1 知识管理资料   基础资料,见附表1 2 知识主题(107301) KMBlg:主题 K ...

  5. Django之admin

    django amdin是django提供的一个后台管理页面,改管理页面提供完善的html和css,使得你在通过Model创建完数据库表之后, 就可以对数据进行增删改查,而使用django admin ...

  6. Think PHP 基础

    ThinkPHP 一.什么框架: 一堆代码的集合,里边有变量.函数.类.常量,里边也有许多设计模式MVC.AR数据库.单例等等. 框架可以节省我们50-60%的工作量,我们全部精力都集中在业务层次. ...

  7. linux下实现自动部署tomcat的脚本

    linux下实现自动部署tomcat的脚本 由于经常部署war到tomccat上,经常有一些重复的工作要做:停服务.备份war包.上传新的war包.启动服务.索性就写了一个自动部署的脚本. 脚本如下a ...

  8. Pow(x, n) leetcode

    Implement pow(x, n). Subscribe to see which companies asked this question 利用依次消去二进制位上的1,来进行计算 double ...

  9. 1342: [Baltic2007]Sound静音问题

    1342: [Baltic2007]Sound静音问题 Time Limit: 5 Sec  Memory Limit: 162 MBSubmit: 710  Solved: 307[Submit][ ...

  10. idea中编译项目报错 java: javacTask: 源版本 1.8 需要目标版本 1.8

    问题如上面所叙: > idea中编译项目报错 java: javacTask: 源版本 1.8 需要目标版本 1.8 解决方案: > Setting->Compiler->Ja ...