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

简介: 1. 问题的引入——派生类不会发现模板基类中的名字 假设我们需要写一个应用,使用它可以为不同的公司发送消息。消息可以以加密或者明文(未加密)的方式被发送。如果在编译阶段我们有足够的信息来确定哪个信息会被发送到哪个公司,我们可以使用基于模板的解决方案:   1 class CompanyA { 2 public: 3 .

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
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  
36 
37 { ... }                                              // calls c.sendEncrypted
38 
39 }        

                                       

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

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

 

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

2. 问题分析

2.1 一般化分析

问题出现在当编译器遇到类模版LoggingMsgSender的定义时,它们不知道它继承自什么类。当然,它是继承自MsgSender,但是Company是一个模板参数,这个参数只有在LoggingMsgSender被实例化的时候才会被确认。在不知道Company是什么的情况下,我们也不知道MsgSender是什么样子的。因此也就没有方法获知是否存在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 {      // MsgSender; the same as the
4 
5 
6 public: // general template, except
7 ... // sendClear is omitted
8 void sendSecret(const MsgInfo& info)
9 { ... }

 

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

 

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

 

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

 

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

 

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

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

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

 

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

 

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

1 LoggingMsgSender zMsgSender;
2 MsgInfo msgData;
3 ...                                                                  // put info in msgData
4 
5 zMsgSender.sendClearMsg(msgData);         // error! won’t compile

 

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

 

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

 

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

 

5. 总结

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


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
2月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
112 10
|
4月前
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
26天前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
42 4
|
26天前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
34 3
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
29天前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
31 0
|
2月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
18 1
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
60 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
88 2
|
2月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
45 2