![]() | |
![]() ![]() ![]() |
![]() | Imperfect C++ Practical Solutions for Real-Life Programming By Matthew Wilson |
Table of Contents | |
Chapter 18. Typedefs |
18.5. The Good, the Bad, and the UglyThis chapter is looking at typedefs and, in the main, lauding the typedef specifier as a very useful tool. However, I'd just like to have a small soapbox moment and comment on some common mistakes that often befall the overeager typedef convert 18.5.1 Good typedefsGood typedefs are those that reduce typing, increase portability, or increase flexibility, or all of these things at once. For example, it is a matter of habit when I define any class that inherits from another that I declare a typedef base_class_type, as in: template< typename T , typename A = std::allocator<T> > class acme_stack : protected std::vector<T, A> { private: typedef std::vector<T, A> base_class_type; public: typedef typename base_class_type::reference reference; typedef typename base_class_type::const_reference const_reference; . . . This has two advantages. First, whenever I want to implement a member type or function of acme_stack in terms of std::vector<T, A> I can type base_class_type. Now admitting that in this case there is not a great saving in terms of the number of characters typed, the typing is nevertheless easier because we do not have to handle the braces and those messy double colons! In many cases, though, there is a dramatic saving in typing; the longest base class that I could find in the Synesis code base is: ComRefCounter< ComRefCounterBase<IFileUtil> , ComQI3< IFileUtil2 , &IID_IFileUtil2 , IFileUtil , &IID_IFileUtil , IDispatch , &IID_IDispatch , ComRefCounterBase<IFileUtil> > > Try replicating that throughout a class's implementation without making at least one mistake! (And remember that if your class is itself a template, then any mistakes will not show up until the particular member function is called, which can be some time after you'd "completed" and released it if your unit tests are missing 100% code coverage, which of course they are [Glas2003].) The downside of this is that sometimes you may have more than one base type. However, if you wisely eschew using heavy multiple inheritance [Stro1997]—that is, where more than one base type provides significant members and behavior—then in almost all cases there is one identifiable base class that your class has an IsA [Meye1998] relationship with, and the remaining base types are used to provide policy-based behavior or to declare interfaces. The other great use of base_class_type is that it simplifies the task of changing a base class in an inheritance hierarchy. Let's forget all about templates for the moment, and just think about a plain polymorphic set of classes. class Window { . . . }; class Control : public Window { . . . }; class FileListControl : public Control { DECLARE_MESSAGE_HANDLERS(FileListControl, Control) . . . // in FileListControl.cpp bool FileListControl::Create(Window const &parent, . . .) { . . . // modify other-parameters return Control::Create(parent, other-parameters); } As is the nature of such hierarchies, let's assume heaps of macros within the declaration and definition of these classes (we'll presume they're wizard generated and are correct at the time they're generated). There can be many tens, or even hundreds, of references to Control within the declaration and definition of FileListControl. Of course, in a next revision of the libraries, a new window class, THReeDEffectControl, is introduced, which derives from Control. Your manager thinks the 3D effects of this new control are the cat's meow, wants to see it in the FileListControl—and the 30 or 40 other controls that your former coworkers left with you when they left the company to join a startup doing SOAP-based multimedia instant messaging J2EE XML Web Services over WAP—and instructs you to "make it so!" You're in a pickle. You could do a global checkout from source control, search for every Control, and replace it with THReeDEffectControl, but there are hundreds of classes derived from Control that want to stay just the way they are. So you're left with a manual task. Well, you can at least locate the header files for the controls you need to change. You make the changes, and now FileListControl and its buddies all derive from ThreeD EffectControl. Alas, as you're about to begin the changes to all the corresponding implementation files, your friendly manager rings and tells you he's sending you to a client site for the next three days to help locate a bug, and "you leave in 20 minutes." I'm sure you get the picture. You get back to the control update task early next week, and make all the changes to the implementation files. Alas, you missed one file, in a rarely used component, and a couple of months later you're on another client site trying to find an intermittent crash when the program has been running a long time. The problem is that the system contains one class that has methods that call up to an indirect base (Control) rather than its immediate base (THReeDEffectControl), which leaves system resources unreleased, and they eventually run short and your process crashes. How much easier would it have been if each class had declared a private base_class_type typedef and only ever referred to its base class by that name? Let's look back at our original acme_stack template class. If we wanted to upgrade it to a faster vector than std::vector, then we could do so by making just two simple changes template< typename T , typename A = std::allocator<T> > class acme_stack : protected fastlib::vector<T, A> { private: typedef fastlib::vector<T, A> base_class_type; public: typedef typename base_class_type::reference reference; typedef typename base_class_type::const_reference const_reference; . . . 18.5.2 Bad TypedefsI've seen people get into typedef, and go then quite mad with the power. An example[8] of a bad typedef is something such as the following:
#if defined(UNICODE) typedef wchar_t char_t; #else typedef char char_t; #endif // UNICODE typedef std::string<char_t> string_t; typedef std::vector<string_t>::iterator string_container_iterator_t; typedef std::vector<string_t>::const_iterator string_vector_const_iterator_t; The first two typedefs string_t and string_container_t are perfectly appropriate. It may not be entirely necessary to define string_t, but it does help to avoid the irritating chevron parsing error that catches template newbies: typedef std::vector<std::string<char_t::iterator string_container_iterator_t; ^ compiler thinks you're right shifting! The problem with the above set of definitions is the iterator typedefs. The whole point of the STL container member type iterator is that it is a contextual typedef. If we then define and use another typedef to represent that, we move the context of the conceptual type definition from std::vector<string_t>, where it belongs, to the global namespace, where it most certainly does not. Imagine some client code: void dump_to_debugger(std::vector<string_t> const &sv, char const *message) { string_container_const_iterator_t begin = sv.begin(); string_container_const_iterator_t end = sv.end(); for(int i = 0; begin != end; ++begin, ++i) { printf("%s, %d: %s\n", message, i, (*begin).c_str()); The definition of string_container_const_iterator_t is brittle. If we change the definition of std::vector<string_t> (e.g., if we want to use a faster vector), then string_container_const_iterator_t may no longer be compatible with the new vector template's iterator type. It's actually even worse if it is compatible, since this nasty wart gets hidden, and the programmer who uses it is not schooled against this bad technique. A slightly milder form of this problem, which I've also seen a fair bit of in the real world, is: typedef std::vector<string_t> string_container_t; typedef string_container_t::iterator string_container_iterator_t; typedef string_container_t::const_iterator string_container_const_iterator_t; In this case, the code will continue to work when string_container_t is changed, but we're still masking the fact that iterator is a contextual type definition and propagating a bad habit. Also, if someone defines a class with a member type of type string_container_t, and implements some functionality in terms of string_container_(const_)iterator_t, then they'll end up back at the inconsistency problem as soon as they need to change their type from string_container_t to something else, for example, string_list_t.
class X
{
public:
// typedef string_container_t container_t; // Was this,
typedef string_list_t container_t; // upgraded to this
. . .
void dump(constainer_t const &c) const
{
string_container_const_iterator_t begin = c.begin() // Broken!
The only time this lesser form of typedef is valid is within a function or template algorithm where the type of the containing type is known and the derivative types are visible within a limited scope, as in: template <typename C> void dump_container(C const &c) { typedef typename C::const_iterator iter_t; iter_t begin = c.begin(); . . . } 18.5.3 Dubious Typedefs
As I mentioned in section 2.2, declaring (but not defining) private methods may be used to prevent copy construction and/or copy assignment. When dealing with templates, some older compilers (e.g., Visual C++ 4.0, if I remember correctly) had problems deducing the actual operand types when they were specified as follows:
template <typename T>
class X
{
// Construction
public:
X();
explicit X(int i);
// Not to be implemented
private:
X(X const &); // Confusion here
};
The problem was that some compilers could deduce that X, as a type within the scope of X<T>, actually meant X<T>, which is the interpretation that all modern compilers must make. Others could not, and either had a compiler error, or treated X as another type (of what exact type I could never determine), which meant that in the earlier case the copy constructor would not be properly declared and would, therefore, be automatically generated (see section 2.2) by the compiler! The answer I came up with was pretty simple: template <typename T> class X { public: typedef X<T> class_type; . . . // Not to be implemented private: X(class_type const &); // No more confusion }; I've been in the same habit ever since, and I almost always define a class_type for every class. It means that I can write the canonical prohibited copy operations in the above form without thinking about it much. (Of course, that's probably not entirely a good thing.) It's also very helpful to maintainability when dealing with nested classes of templates. Consider the following big hairy beast, which we also discuss in section 20.5: template< typename S /* string type, e.g string */ , typename D /* delimiter type, e.g. char or wstring */ , typename B = string_tokeniser_ignore_blanks<true> , typename V = S /* value type */ , typename T = string_tokeniser_type_traits<S, V> , typename P = string_tokeniser_comparator<D, S, T> > class string_tokeniser { public: typedef string_tokeniser<S, D, B, V, T, P> tokeniser_type; . . . class const_iterator { . . . // Members private: tokeniser_type *const m_tokeniser; } }; In this case the string_tokeniser template provides a "class_type" member type in the form of tokeniser_type. This is then visible to the nested class const_iterator, which maintains a back pointer to the string_tokeniser instance for which it is an iterator, in order that it may access the tokenized string to advance its position and return token values. If the member type tokeniser_type was not defined, then the const_iterator nested class would have to stipulate string_tokeniser<S, D, B, V, T, P>, and you can imagine how easy it is to get that out of step.[9]
Finally, the class_type nested type can also be useful when one is using macros[10] within a class definition, and we'll see an excellent example of this in the discussion of properties in Chapter 35.
All the typedefs in this item so far have been pretty justifiable, but I want to finish on another dubious typedef, this one being much less justifiable than class_type. In a Java parsing tool I wrote—in C++, of course—I made the reporter and modifier components conform to an agreed binary standard (see Chapter 8), so I could execute different tools over a Java source tree. It works very well, and can spot unused variables, redundant imports, and inefficient string concatenation in source code trees. It can even change the bracing styles of source trees containing a million lines of code in just a few minutes, which is much quicker than it can take to debate such matters. Since each Reporter/transformer derived class contains very similar code, insofar as one or both of two member functions are redefined, I was doing a great deal of copy and paste. It gets deadly dull making the same changes in file after file, and there wasn't quite enough justification to make a code generator [Hunt2000], so I used a somewhat nasty but highly effective technique. At the head of each implementation file—one could never conscience such a thing in header files!—is a definition of a contextual typedef LocalClass, which is defined to be the class corresponding to the implementation file. For example: // PackageDependency.cpp #include . . . typedef PackageDependency LocalClass; // BraceInserter.cpp #include . . . typedef BraceInserter LocalClass; All the remaining references to the particular implementation class are via the type LocalClass, which cuts down the repetitive effort. There are no code correctness problems because the headers were constructed manually, and with due care. The other benefit was that it was really useful when using a visual differencing tool to compare the implementations of, say, the JavaDocInserter and the DoxygenInserter classes, since the only differences were functional ones. The class names prefixing the method definitions are all LocalClass::. Anyway, I'm not going to try too hard to sell you on this one, but I thought it was worth drawing your attention to a further example of the power of typedef. Wield it carefully! |
![]() | |
![]() ![]() ![]() |