1. 程式人生 > 其它 >Effective C++ 筆記 —— Item 48: Be aware of template metaprogramming.

Effective C++ 筆記 —— 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.

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 ++ or -- for other iterator categories
         else { while (d++) --iter; }
     }
 }

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
  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! won’t compile
    }
    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::iterator, but list::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::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 factorial computation demonstrates looping through recursive template instantiation. It also demonstrates one way in which variables are created and used in TMP. Look:

// general case: the value of Factorial<n> is n times the value of Factorial<n-1>
template<unsigned n> 
struct Factorial {
    enum { value = n * Factorial<n - 1>::value };
};

// special case: the value of Factorial<0> is 1
template<> 
struct Factorial<0> {
    enum { value = 1 };
};

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
}

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.