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

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

  1. class CompanyA {
  2. public:
  3. ...
  4. void sendCleartext(const std::string& msg);
  5. void sendEncrypted(const std::string& msg);
  6. ...
  7. };
  8. class CompanyB {
  9. public:
  10. ...
  11. void sendCleartext(const std::string& msg);
  12. void sendEncrypted(const std::string& msg);
  13. ...
  14. };
  15.  
  16. ... // classes for other companies
  17.  
  18. class MsgInfo { ... }; // class for holding information
  19. // used to create a message
  20.  
  21. template<typename Company>
  22. class MsgSender {
  23. public:
  24. ... // ctors, dtor, etc.
  25.  
  26. void sendClear(const MsgInfo& info)
  27. {
  28. std::string msg;
  29. create msg from info;
  30. Company c;
  31. c.sendCleartext(msg);
  32. }
  33. void sendSecret(const MsgInfo& info) // similar to sendClear, except
  34.  
  35. { ... } // calls c.sendEncrypted
  36.  
  37. }

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

  1. template<typename Company>
  2.  
  3. class LoggingMsgSender: public MsgSender<Company> {
  4.  
  5. public:
  6.  
  7. ... // ctors, dtor, etc.
  8.  
  9. void sendClearMsg(const MsgInfo& info)
  10.  
  11. {
  12.  
  13. write "before sending" info to the log;
  14.  
  15. sendClear(info); // call base class function;
  16. // this code will not compile!
  17. write "after sending" info to the log;
  18. }
  19. ...
  20. };

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

2. 问题分析

2.1 一般化分析

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

2.2 用实例来证明问题所在

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

  1. class CompanyZ { // this class offers no
  2.  
  3. public: // sendCleartext function
  4.  
  5. ...
  6.  
  7. void sendEncrypted(const std::string& msg);
  8.  
  9. ...
  10.  
  11. };

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

  1. template<> // a total specialization of
  2.  
  3. class MsgSender<CompanyZ> { // MsgSender; the same as the
  4.  
  5. public: // general template, except
  6. ... // sendClear is omitted
  7. void sendSecret(const MsgInfo& info)
  8. { ... }

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

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

  1. template<typename Company>
  2. class LoggingMsgSender: public MsgSender<Company> {
  3. public:
  4. ...
  5. void sendClearMsg(const MsgInfo& info)
  6. {
  7. write "before sending" info to the log;
  8. sendClear(info); // if Company == CompanyZ,
  9. // this function doesn’t exist!
  10. write "after sending" info to the log;
  11. }
  12. ...
  13. };

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

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

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

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

  1. template<typename Company>
  2. class LoggingMsgSender: public MsgSender<Company> {
  3. public:
  4. ...
  5. void sendClearMsg(const MsgInfo& info)
  6. {
  7. write "before sending" info to the log;
  8. this->sendClear(info); // okay, assumes that
  9. // sendClear will be inherited
  10. write "after sending" info to the log;
  11. }
  12. ...
  13. };

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

  1. template<typename Company>
  2. class LoggingMsgSender: public MsgSender<Company> {
  3. public:
  4. using MsgSender<Company>::sendClear; // tell compilers to assume
  5. ... // that sendClear is in the
  6. // base class
  7. void sendClearMsg(const MsgInfo& info)
  8. {
  9. ...
  10.  
  11. sendClear(info); // okay, assumes that
  12.  
  13. ... // sendClear will be inherited
  14.  
  15. }
  16.  
  17. ...
  18.  
  19. };

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

  1. template<typename Company>
  2.  
  3. class LoggingMsgSender: public MsgSender<Company> {
  4.  
  5. public:
  6.  
  7. ...
  8.  
  9. void sendClearMsg(const MsgInfo& info)
  10.  
  11. {
  12.  
  13. ...
  14.  
  15. MsgSender<Company>::sendClear(info);
  16.  
  17. // okay, assumes that
  18.  
  19. ... // sendClear will be
  20. } // inherited
  21. ...
  22. };

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

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

  1. LoggingMsgSender<CompanyZ> zMsgSender;
  2. MsgInfo msgData;
  3. ... // put info in msgData
  4.  
  5. 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. url传参后获取参数

    当我们通过url传参跳转到其他页面,如: http://www.xxx.com/content.html?id=217&name=txf&phone=15829087165 在跳转后的 ...

  2. linux下apache 的安装

    1.进入work目录下:cd /usr/local/work(如没有则自己新建,命令:mkdir /usr/local/work) 2.在woke目录下从网站下载apache并解压:wget http ...

  3. Sqlite 帮助类 SQLiteHelper

    ///源码下载地址:http://download.csdn.net/detail/kehaigang29/8836171 ///dll下载地址:http://download.csdn.net/de ...

  4. Django实现用户注册登录

    学习Django中:试着着写一个用户注册登录系统,开始搞事情 =====O(∩_∩)O哈哈~===== ================= Ubuntu python 2.7.12 Django 1. ...

  5. Linux下ls命令显示符号链接权限为777的探索

    Linux下ls命令显示符号链接权限为777的探索 --深入ls.链接.文件系统与权限 一.摘要 ls是Linux和Unix下最常使用的命令之一,主要用来列举目录下的文件信息,-l参数允许查看当前目录 ...

  6. DOM操作表格——HTML DOM

    html创建表格: <table berder='1' width='300'> <thead> <tr> <th>姓名</th> < ...

  7. 整理 - .Net系统预定义的委托们

    大部分情况下,我们可以使用系统预定义的委托类型,而不需要自己再来手动定义. 以下委托都位于System命名空间下. 传入参数.返回值的类型,大都被声明为泛型. 1.Action系列 有0-16个参数, ...

  8. Linux 下查看CPU的使用情况

    1.top使用权限:所有使用者使用方式:top [-] [d delay] [q] [c] [S] [s] [i] [n] [b]说明:即时显示process的动态d :改变显示的更新速度,或是在交谈 ...

  9. 细谈position属性:static、fixed、relative与absolute

    学习WEB有些时日了,对DOM中的定位概念有些模糊,特地花了一个下午的时间搜资料.整理写下这篇随笔. 首先,我们要清楚一个概念:文档流. 简单的讲,就是窗体自上而下分成一行一行,并在每行中按照从左到右 ...

  10. AlloyTouch.js 源码 学习笔记及原理说明

    alloyTouch这个库其实可以做很多事的, 比较抽象, 需要我们用户好好的思考作者提供的实例属性和一些回调方法(touchStart, change, touchMove, pressMove, ...