T.meta: Template metaprogramming (TMP)

Templates provide a general mechanism for compile-time programming.

Metaprogramming is programming where at least one input or one result is a type. Templates offer Turing-complete (modulo memory capacity) duck typing at compile time. The syntax and techniques needed are pretty horrendous.

T.120: Use template metaprogramming only when you really need to

Reason

Template metaprogramming is hard to get right, slows down compilation, and is often very hard to maintain. However, there are real-world examples where template metaprogramming provides better performance that any alternative short of expert-level assembly code. Also, there are real-world examples where template metaprogramming expresses the fundamental ideas better than run-time code. For example, if you really need AST manipulation at compile time (e.g., for optional matrix operation folding) there may be no other way in C++.

Example, bad
???
Example, bad
enable_if

Instead, use concepts. But see How to emulate concepts if you don't have language support.

Example
??? good

Alternative: If the result is a value, rather than a type, use a constexpr function.

Note

If you feel the need to hide your template metaprogramming in macros, you have probably gone too far.

T.121: Use template metaprogramming primarily to emulate concepts

Reason

Until concepts become generally available, we need to emulate them using TMP. Use cases that require concepts (e.g. overloading based on concepts) are among the most common (and simple) uses of TMP.

Example
template<typename Iter>
    /*requires*/ enable_if<random_access_iterator<Iter>, void>
advance(Iter p, int n) { p += n; }

template<typename Iter>
    /*requires*/ enable_if<forward_iterator<Iter>, void>
advance(Iter p, int n) { assert(n >= 0); while (n--) ++p;}
Note

Such code is much simpler using concepts:

void advance(RandomAccessIterator p, int n) { p += n; }

void advance(ForwardIterator p, int n) { assert(n >= 0); while (n--) ++p;}
Enforcement

???

T.122: Use templates (usually template aliases) to compute types at compile time

Reason

Template metaprogramming is the only directly supported and half-way principled way of generating types at compile time.

Note

"Traits" techniques are mostly replaced by template aliases to compute types and constexpr functions to compute values.

Example
??? big object / small object optimization
Enforcement

???

T.123: Use constexpr functions to compute values at compile time

Reason

A function is the most obvious and conventional way of expressing the computation of a value. Often a constexpr function implies less compile-time overhead than alternatives.

Note

"Traits" techniques are mostly replaced by template aliases to compute types and constexpr functions to compute values.

Example
template<typename T>
    // requires Number<T>
constexpr T pow(T v, int n)   // power/exponential
{
    T res = 1;
    while (n--) res *= v;
    return res;
}

constexpr auto f7 = pow(pi, 7);
Enforcement
* Flag template metaprograms yielding a value. These should be replaced with `constexpr` functions.

T.124: Prefer to use standard-library TMP facilities

Reason

Facilities defined in the standard, such as conditional, enable_if, and tuple, are portable and can be assumed to be known.

Example
???
Enforcement

???

T.125: If you need to go beyond the standard-library TMP facilities, use an existing library

Reason

Getting advanced TMP facilities is not easy and using a library makes you part of a (hopefully supportive) community. Write your own "advanced TMP support" only if you really have to.

Example
???
Enforcement

???

Other template rules

T.140: Name all nontrivial operations

Reason

Documentation, readability, opportunity for reuse.

Example
???
Example, good
???
Note

whether functions, lambdas, or operators.

Exceptions
  • Lambdas logically used only locally, such as an argument to for_each and similar control flow algorithms.
  • Lambdas as initializers
Enforcement

???

T.141: Use an unnamed lambda if you need a simple function object in one place only

Reason

That makes the code concise and gives better locality than alternatives.

Example
auto earlyUsersEnd = std::remove_if(users.begin(), users.end(),
                                    [](const User &a) { return a.id > 100; });

Exception: Naming a lambda can be useful for clarity even if it is used only once

Enforcement
  • Look for identical and near identical lambdas (to be replaced with named functions or named lambdas).

T.142?: Use template variables to simplify notation

Reason

Improved readability.

Example
???
Enforcement

???

T.143: Don't write unintentionally nongeneric code

Reason

Generality. Reusability. Don't gratuitously commit to details; use the most general facilities available.

Example

Use != instead of < to compare iterators; != works for more objects because it doesn't rely on ordering.

for (auto i = first; i < last; ++i) {   // less generic
    // ...
}

for (auto i = first; i != last; ++i) {   // good; more generic
    // ...
}

Of course, range-for is better still where it does what you want.

Example

Use the least-derived class that has the functionality you need.

class base {
public:
    void f();
    void g();
};

class derived1 : public base {
public:
    void h();
};

class derived2 : public base {
public:
    void j();
};

void myfunc(derived1& param)  // bad, unless there is a specific reason for limiting to derived1 objects only
{
    use(param.f());
    use(param.g());
}

void myfunc(base& param)   // good, uses only base interface so only commit to that
{
    use(param.f());
    use(param.g());
}
Enforcement
  • Flag comparison of iterators using < instead of !=.
  • Flag x.size() == 0 when x.empty() or x.is_empty() is available. Emptiness works for more containers than size(), because some containers don't know their size or are conceptually of unbounded size.
  • Flag functions that take a pointer or reference to a more-derived type but only use functions declared in a base type.

T.144: Don't specialize function templates

Reason

You can't partially specialize a function template per language rules. You can fully specialize a function template but you almost certainly want to overload instead -- because function template specializations don't participate in overloading, they don't act as you probably wanted. Rarely, you should actually specialize by delegating to a class template that you can specialize properly.

Example
???

Exceptions: If you do have a valid reason to specialize a function template, just write a single function template that delegates to a class template, then specialize the class template (including the ability to write partial specializations).

Enforcement
  • Flag all specializations of a function template. Overload instead.