Item 43: Know how to access names in templatized base classes


Suppose we need to write an application that can send messages to several different companies. Messages can be sent in either encrypted or cleartext (unencrypted) form. If we have enough information during compilation to determine which messages will go to which companies, we can employ a template-based solution:

 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 }; 

This will work fine, but suppose we sometimes want to log some information each time we send a message. A derived class can easily add that capability, and this seems like a reasonable way to do it:

 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;   }   ... }; 

Note how the message-sending function in the derived class has a different name (sendClearMsg) from the one in its base class (there, it's called sendClear). That's good design, because it side-steps the issue of hiding inherited names (see Item 33) as well as the problems inherent in redefining an inherited non-virtual function (see Item 36). But the code above won't compile, at least not with conformant compilers. Such compilers will complain that sendClear doesn't exist. We can see that sendClear is in the base class, but compilers won't look for it there. We need to understand why.

The problem is that when compilers encounter the definition for the class template LoggingMsgSender, they don't know what class it inherits from. Sure, it's MsgSender<Company>, but Company is a template parameter, one that won't be known until later (when LoggingMsgSender is instantiated). Without knowing what Company is, there's no way to know what the class MsgSender<Company> looks like. In particular, there's no way to know if it has a sendClear function.

To make the problem concrete, suppose we have a class CompanyZ that insists on encrypted communications:

 class CompanyZ {                             // this class offers no public:                                      // sendCleartext function   ...   void sendEncrypted(const std::string& msg);   ... }; 

The general MsgSender template is inappropriate for CompanyZ, because that template offers a sendClear function that makes no sense for CompanyZ objects. To rectify that problem, we can create a specialized version of MsgSender for CompanyZ:

 template<>                                 // a total specialization of class MsgSender<CompanyZ> {                // MsgSender; the same as the public:                                    // general template, except   ...                                      // sendCleartext is omitted   void sendSecret(const MsgInfo& info)   { ... } }; 

Note the "template <>" syntax at the beginning of this class definition. It signifies that this is neither a template nor a standalone class. Rather, it's a specialized version of the MsgSender template to be used when the template argument is CompanyZ. This is known as a total template specialization: the template MsgSender is specialized for the type CompanyZ, and the specialization is total once the type parameter has been defined to be CompanyZ, no other aspect of the template's parameters can vary.

Given that MsgSender has been specialized for CompanyZ, consider again the derived class 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;   }   ... }; 

As the comment notes, this code makes no sense when the base class is MsgSender<CompanyZ>, because that class offers no sendClear function. That's why C++ rejects the call: it recognizes that base class templates may be specialized and that such specializations may not offer the same interface as the general template. As a result, it generally refuses to look in templatized base classes for inherited names. In some sense, when we cross from Object-oriented C++ to Template C++ (see Item 1), inheritance stops working.

To restart it, we have to somehow disable C++'s "don't look in templatized base classes" behavior. There are three ways to do this. First, you can preface calls to base class functions with "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;   }   ... }; 

Second, you can employ a using declaration, a solution that should strike you as familiar if you've read Item 33. That Item explains how using declarations bring hidden base class names into a derived class's scope. We can therefore write sendClearMsg like this:

 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   }   ... }; 

(Although a using declaration will work both here and in Item 33, the problems being solved are different. Here, the situation isn't that base class names are hidden by derived class names, it's that compilers don't search base class scopes unless we tell them to.)

A final way to get your code to compile is to explicitly specify that the function being called is in the base class:

 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   ... }; 

This is generally the least desirable way to solve the problem, because if the function being called is virtual, explicit qualification turns off the virtual binding behavior.

From a name visibility point of view, each of these approaches does the same thing: it promises compilers that any subsequent specializations of the base class template will support the interface offered by the general template. Such a promise is all compilers need when they parse a derived class template like LoggingMsgSender, but if the promise turns out to be unfounded, the truth will emerge during subsequent compilation. For example, if the source code later contains this,

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

the call to sendClearMsg won't compile, because at this point, compilers know that the base class is the template specialization MsgSender<CompanyZ>, and they know that class doesn't offer the sendClear function that sendClearMsg is trying to call.

Fundamentally, the issue is whether compilers will diagnose invalid references to base class members sooner (when derived class template definitions are parsed) or later (when those templates are instantiated with specific template arguments). C++'s policy is to prefer early diagnoses, and that's why it assumes it knows nothing about the contents of base classes when those classes are instantiated from templates.

Things to Remember

  • In derived class templates, refer to names in base class templates via a "this->" prefix, via using declarations, or via an explicit base class qualification.




Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
ISBN: 321334876
EAN: N/A
Year: 2006
Pages: 102

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net