More Books
Effective C++ 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Table of Contents
Copyright
Praise for Effective C++, Third Edition
Addison-Wesley Professional Computing Series
Preface
Acknowledgments
Introduction
Terminology
Chapter 1. Accustoming Yourself to C++
Item 1: View C++ as a federation of languages
Item 2: Prefer consts, enums, and inlines to #defines
Item 3: Use const whenever possible
Item 4: Make sure that objects are initialized before they're used
Chapter 2. Constructors, Destructors, and Assignment Operators
Item 5: Know what functions C++ silently writes and calls
Item 6: Explicitly disallow the use of compiler-generated functions you do not want
Item 7: Declare destructors virtual in polymorphic base classes
Item 8: Prevent exceptions from leaving destructors
Item 9: Never call virtual functions during construction or destruction
Item 10: Have assignment operators return a reference to *this
Item 11: Handle assignment to self in operator=
Item 12: Copy all parts of an object
Chapter 3. Resource Management
Item 13: Use objects to manage resources.
Item 14: Think carefully about copying behavior in resource-managing classes.
Item 15: Provide access to raw resources in resource-managing classes.
Item 16: Use the same form in corresponding uses of new and delete.
Item 17: Store newed objects in smart pointers in standalone statements.
Chapter 4. Designs and Declarations
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
Item 19: Treat class design as type design
Item 20: Prefer pass-by-reference-to-const to pass-by-value
Item 21: Don't try to return a reference when you must return an object
Item 22: Declare data members private
Item 23: Prefer non-member non-friend functions to member functions
Item 24: Declare non-member functions when type conversions should apply to all parameters
Item 25: Consider support for a non-throwing swap
Chapter 5. Implementations
Item 26: Postpone variable definitions as long as possible.
Item 27: Minimize casting.
Item 28: Avoid returning "handles" to object internals.
Item29: Strive for exception-safe code.
Item 30: Understand the ins and outs of inlining.
Item31: Minimize compilation dependencies between files.
Chapter 6. Inheritance and Object-Oriented Design
Item 32: Make sure public inheritance models "is-a."
Item 33: Avoid hiding inherited names
Item 34: Differentiate between inheritance of interface and inheritance of implementation
Item 35: Consider alternatives to virtual functions
Item 36: Never redefine an inherited non-virtual function
Item 37: Never redefine a function's inherited default parameter value
Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition
Item 39: Use private inheritance judiciously
Item 40: Use multiple inheritance judiciously
Chapter 7. Templates and Generic Programming
Item 41: Understand implicit interfaces and compile-time polymorphism
Item 42: Understand the two meanings of typename
Item 43: Know how to access names in templatized base classes
Item 44: Factor parameter-independent code out of templates
Item 45: Use member function templates to accept "all compatible types."
Item 46: Define non-member functions inside templates when type conversions are desired
Item 47: Use traits classes for information about types
Item 48: Be aware of template metaprogramming
Chapter 8. Customizing new and delete
Item 49: Understand the behavior of the new-handler
Item 50: Understand when it makes sense to replace new and delete
Item 51: Adhere to convention when writing new and delete
Item 52: Write placement delete if you write placement new
Chapter 9. Miscellany
Item 53: Pay attention to compiler warnings.
Item 54: Familiarize yourself with the standard library, including TR1
Item.55: Familiarize yourself with Boost.
Appendix A. Beyond Effective C++
Appendix B. Item Mappings Between Second and Third Editions
Index
index_SYMBOL
index_A
index_B
index_C
index_D
index_E
index_F
index_G
index_H
index_I
index_J
index_K
index_L
index_M
index_N
index_O
index_P
index_R
index_S
index_T
index_U
index_V
index_W
index_X
index_Z

Item 48: Be aware of template metaprogramming

Template metaprogramming (TMP) is the process of writing template-based C++ programs that execute during compilation. Think about that for a minute: a template metaprogram is a program written in C++ that executes inside the C++ compiler. When a TMP program finishes running, its output — pieces of C++ source code instantiated from templates — is then compiled as usual.

If this doesn't strike you as just plain bizarre, you're not thinking about it hard enough.

C++ was not designed for template metaprogramming, but since TMP was discovered in the early 1990s, it has proven to be so useful, extensions are likely to be added to both the language and its standard library to make TMP easier. Yes, TMP was discovered, not invented. The features underlying TMP were introduced when templates were added to C++. All that was needed was for somebody to notice how they could be used in clever and unexpected ways.

TMP has two great strengths. First, it makes some things easy that would otherwise be hard or impossible. Second, because template metaprograms execute during C++ compilation, they can shift work from runtime to compile-time. One consequence is that some kinds of errors that are usually detected at runtime can be found during compilation. Another is that C++ programs making use of TMP can be more efficient in just about every way: smaller executables, shorter runtimes, lesser memory requirements. (However, a consequence of shifting work from runtime to compile-time is that compilation takes longer. Programs using TMP may take much longer to compile than their non-TMP counterparts.)

Consider the pseudocode for STL's advance introduced on page 228. (That's in Item 47. You may want to read that Item now, because in this Item, I'll assume you are familiar with the material in that one.) As on page 228, I've highlighted the pseudo part of the code:


template<typename IterT, typename DistT>

void advance(IterT& iter, DistT d)

{

  if (iter is a random access iterator) {

    iter += d;                                     // use iterator arithmetic

  }                                                // for random access iters

  else {

    if (d >= 0) { while (d--) ++iter; }            // use iterative calls to

    else { while (d++) --iter; }                   // ++ or -- for other

  }                                                // iterator categories

}


We can use typeid to make the pseudocode real. That yields a "normal" C++ approach to this problem — one that does all its work at runtime:


template<typename IterT, typename DistT>

void advance(IterT& iter, DistT d)

{

  if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==

      typeid(std::random_access_iterator_tag)) {



     iter += d;                                     // use iterator arithmetic

  }                                                 // for random access iters

  else {

    if (d >= 0) { while (d--) ++iter; }             // use iterative calls to

    else { while (d++) --iter; }                    // ++ or -- for other

  }                                                 // iterator categories

}


Item 47 notes that this typeid-based approach is less efficient than the one using traits, because with this approach, (1) the type testing occurs at runtime instead of during compilation, and (2) the code to do the runtime type testing must be present in the executable. In fact, this example shows how TMP can be more efficient than a "normal" C++ program, because the traits approach is TMP. Remember, traits enable compile-time if...else computations on types.

I remarked earlier that some things are easier in TMP than in "normal" C++, and advance offers an example of that, too. Item 47 mentions that the typeid-based implementation of advance can lead to compilation problems, and here's an example where it does:


std::list<int>::iterator iter;



...



advance(iter, 10);                          // move iter 10 elements forward;

                                            // won't compile with above impl.


Consider the version of advance that will be generated for the above call. After substituting iter's and 10's types for the template parameters IterT and DistT, we get this:


void advance(std::list<int>::iterator& iter, int d)

{

  if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) ==

      typeid(std::random_access_iterator_tag)) {



    iter += d;                                        // error!

  }

  else {

    if (d >= 0) { while (d--) ++iter; }

    else { while (d++) --iter; }

  }

}


The problem is the highlighted line, the one using +=. In this case, we're trying to use += on a list<int>::iterator, but list<int>::iterator is a bidirectional iterator (see Item 47), so it doesn't support +=. Only random access iterators support +=. Now, we know we'll never try to execute the += line, because the typeid test will always fail for list<int>::iterators, but compilers are obliged to make sure that all source code is valid, even if it's not executed, and "iter += d" isn't valid when iter isn't a random access iterator. Contrast this with the traits-based TMP solution, where code for different types is split into separate functions, each of which uses only operations applicable to the types for which it is written.

TMP has been shown to be Turing-complete, which means that it is powerful enough to compute anything. Using TMP, you can declare variables, perform loops, write and call functions, etc. But such constructs look very different from their "normal" C++ counterparts. For example, Item 47 shows how if...else conditionals in TMP are expressed via templates and template specializations. But that's assembly-level TMP. Libraries for TMP (e.g., Boost's MPL — see Item 55) offer a higher-level syntax, though still not something you'd mistake for "normal" C++.

For another glimpse into how things work in TMP, let's look at loops. TMP has no real looping construct, so the effect of loops is accomplished via recursion. (If you're not comfortable with recursion, you'll need to address that before venturing into TMP. It's largely a functional language, and recursion is to functional languages as TV is to American pop culture: inseparable.) Even the recursion isn't the normal kind, however, because TMP loops don't involve recursive function calls, they involve recursive template instantiations.

The "hello world" program of TMP is computing a factorial during compilation. It's not a very exciting program, but then again, neither is "hello world," yet both are helpful as language introductions. TMP factorial computation demonstrates looping through recursive template instantiation. It also demonstrates one way in which variables are created and used in TMP. Look:


template<unsigned n>                 // general case: the value of

struct Factorial {                   // Factorial<n> is n times the value

                                     // of Factorial<n-1>



  enum { value = n * Factorial<n-1>::value };



};



template<>                           // special case: the value of

struct Factorial<0> {                // Factorial<0> is 1

  enum { value = 1 };



};


Given this template metaprogram (really just the single template metafunction Factorial), you get the value of factorial(n) by referring to Factorial<n>::value.

The looping part of the code occurs where the template instantiation Factorial<n> references the template instantiation Factorial<n-1>. Like all good recursion, there's a special case that causes the recursion to terminate. Here, it's the template specialization Factorial<0>.

Each instantiation of the Factorial template is a struct, and each struct uses the enum hack (see Item 2) to declare a TMP variable named value. value is what holds the current value of the factorial computation. If TMP had a real looping construct, value would be updated each time around the loop. Since TMP uses recursive template instantiation in place of loops, each instantiation gets its own copy of value, and each copy has the proper value for its place in the "loop."

You could use Factorial like this:


int main()

{

  std::cout << Factorial<5>::value;            // prints 120

  std::cout << Factorial<10>::value;           // prints 3628800

}


If you think this is cooler than ice cream, you've got the makings of a template metaprogrammer. If the templates and specializations and recursive instantiations and enum hacks and the need to type things like Factorial<n-1>::value make your skin crawl, well, you're a pretty normal C++ programmer.

Of course, Factorial demonstrates the utility of TMP about as well as "hello world" demonstrates the utility of any conventional programming language. To grasp why TMP is worth knowing about, it's important to have a better understanding of what it can accomplish. Here are three examples:

  • Ensuring dimensional unit correctness. In scientific and engineering applications, it's essential that dimensional units (e.g., mass, distance, time, etc.) be combined correctly. Assigning a variable representing mass to a variable representing velocity, for example, is an error, but dividing a distance variable by a time variable and assigning the result to a velocity variable is fine. Using TMP, it's possible to ensure (during compilation) that all dimensional unit combinations in a program are correct, no matter how complex the calculations. (This is an example of how TMP can be used for early error detection.) One interesting aspect of this use of TMP is that fractional dimensional exponents can be supported. This requires that such fractions be reduced during compilation so that compilers can confirm, for example, that the unit time1/2 is the same as time4/8.

  • Optimizing matrix operations. Item 21 explains that some functions, including operator*, must return new objects, and Item 44 introduces the SquareMatrix class, so consider the following code:

    
    typedef SquareMatrix<double, 10000> BigMatrix;
    
    BigMatrix m1, m2, m3, m4, m5;               // create matrices and
    
    ...                                         // give them values
    
    
    
    BigMatrix result = m1 * m2 * m3 * m4 * m5;    // compute their product
    
    

    Calculating result in the "normal" way calls for the creation of four temporary matrices, one for the result of each call to operator*. Furthermore, the independent multiplications generate a sequence of four loops over the matrix elements. Using an advanced template technology related to TMP called expression templates, it's possible to eliminate the temporaries and merge the loops, all without changing the syntax of the client code above. The resulting software uses less memory and runs dramatically faster.

  • Generating custom design pattern implementations. Design patterns like Strategy (see Item 35), Observer, Visitor, etc. can be implemented in many ways. Using a TMP-based technology called policy-based design, it's possible to create templates representing independent design choices ("policies") that can be combined in arbitrary ways to yield pattern implementations with custom behavior. For example, this technique has been used to allow a few templates implementing smart pointer behavioral policies to generate (during compilation) any of hundreds of different smart pointer types. Generalized beyond the domain of programming artifacts like design patterns and smart pointers, this technology is a basis for what's known as generative programming.

TMP is not for everybody. The syntax is unintuitive, and tool support is weak. (Debuggers for template metaprograms? Ha!) Being an "accidental" language that was only relatively recently discovered, TMP programming conventions are still somewhat experimental. Nevertheless, the efficiency improvements afforded by shifting work from runtime to compile-time can be impressive, and the ability to express behavior that is difficult or impossible to implement at runtime is attractive, too.

TMP support is on the rise. It's likely that the next version of C++ will provide explicit support for it, and TR1 already does (see Item 54). Books are beginning to come out on the subject, and TMP information on the web just keeps getting richer. TMP will probably never be mainstream, but for some programmers — especially library developers — it's almost certain to be a staple.

Things to Remember

  • Template metaprogramming can shift work from runtime to compile-time, thus enabling earlier error detection and higher runtime performance.

  • TMP can be used to generate custom code based on combinations of policy choices, and it can also be used to avoid generating code inappropriate for particular types.