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. c#配置文件的简单操作

    // 配置文件 <?xml version="1.0" encoding="utf-8" ?> <configuration> < ...

  2. [看图说话] 基于Spark UI性能优化与调试——初级篇

    Spark有几种部署的模式,单机版.集群版等等,平时单机版在数据量不大的时候可以跟传统的java程序一样进行断电调试.但是在集群上调试就比较麻烦了...远程断点不太方便,只能通过Log的形式,进行分析 ...

  3. web前端页面性能

    前段性能的意义 对于访问一个网站,最花费时间的并不是后端应用程序处理以及数据库等消耗的时间,而是前端花费的时间(包括请求.网络传输.页面加载.渲染等).根据web优化的黄金法则:80%的最终用户响应时 ...

  4. 每天一个Linux命令(13)--less命令

    less 工具也是对文件或其它输出进行分页显示的工具,应该说是Linux正统馋看文件内容的工具,功能极其强大.less 的用法比起  more 更加有弹性.  在 more 的时候,我们没有办法向前面 ...

  5. nginx配置参数详解

    配置参数详解 user nginx nginx ; Nginx用户及组:用户 组.window下不指定 worker_processes 8; 工作进程:数目.根据硬件调整,通常等于CPU数量或者2倍 ...

  6. Java并发之线程异常捕获

    由于线程的本质特性,使得你不能捕获从线程中逃逸的异常,如: import java.util.concurrent.ExecutorService; import java.util.concurre ...

  7. 3406: [Usaco2009 Oct]Invasion of the Milkweed 乳草的入侵

    3406: [Usaco2009 Oct]Invasion of the Milkweed 乳草的入侵 Time Limit: 3 Sec  Memory Limit: 128 MBSubmit: 8 ...

  8. webpack和webpack-dev-server安装配置(遇到各种问题的解决方法)

    跟着Webpack傻瓜式指南(一)这个教程在安装webpack和webpack-dev-server的时候遇到很多问题,查了很多终于一一找到解决办法. 主要参考了这三篇博文: moudule.js:3 ...

  9. latex lstlisting

    转自 http://blog.csdn.net/lydyangliu/article/details/9208635 \usepackage{graphicx}\usepackage{xcolor}\ ...

  10. python + selenium <一>

    python 安装 python 下载地址: http://python.org/getit/ ez_setup.py 下载地址: https://pypi.python.org/packages/s ...