C++: Compile-time programming Techniques

185 阅读2分钟

Disclaim: The following trick list is a summary of the talk of Modern Template Metaprogramming: A Compendium  given by Walter E. Brown. 

Trick 1: Value calculation

template <int N> struct abs { static constexpr int value = N < 0 ? -N : N; };

Trick 2: Recursive value calculation

template <unsigned N> struct Fibonacci { 
    static constexpr unsigned value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
template <> struct Fibonacci<0> { static constexpr unsigned value = 0; };
template <> struct Fibonacci<1> { static constexpr unsigned value = 1; };

Trick 3: Refactor static constexpr value

You can see the some duplicate code on the above examples, which is showed:

static constexpr T value = xxx;

We want to reduce those duplicates by factor out common code as following:

// Type identity
template <typename T> struct type_is { using type = T; };

// Value identity
template <typename Integral, Integral v> struct value_is : type_is<Integral> {
  static constexpr Integral value = v;
};

template <int v> using int_is = value_is<int, v>;

template <unsigned int v> using uint_is = value_is<unsigned int, v>;

template <size_t v> using sizet_is = value_is<size_t, v>;

template <bool b> using bool_is = value_is<bool, b>;

using true_t = bool_is<true>;
using false_t = bool_is<false>;

Then, with above helper code, we can rewrite abs and Fibonacci in following forms:

template <int N> using abs = int_is < N<0 ? -N : N>;

template <unsigned int N>
struct Fibonacci : uint_is<Fibonacci<N - 1>::value + Fibonacci<N - 2>::value> {};

template <> struct Fibonacci<0> : uint_is<0> {};
template <> struct Fibonacci<1> : uint_is<1> {};

We can see, the actual calculation expression is embeded as template argument in int_is<> and uint_is<>.

One more example of GCD:

template <unsigned int M, unsigned int N>
struct GCD : uint_is<GCD<N, M % N>::value> {};
template <unsigned int M> struct GCD<M, 0> : uint_is<M> {};
template <> struct GCD<0, 0> {};

The last specialization of GCD means there is not allowed when M and N both are 0, which will cause compilation error.

Trick 4: Extract type info of C-Array type

template <typename Array> struct Rank : sizet_is<0> {};
template <typename T, size_t N>
struct Rank<T[N]> : sizet_is<Rank<T>::value + 1> {};

template <typename Array> struct NumElements : sizet_is<1> {};
template <typename T, size_t N>
struct NumElements<T[N]> : sizet_is<NumElements<T>::value *(N + 1)> {};

The Rank template is used to get the dimensions of a given array type, while the NumElements template is used to get the number of elements of a given array type.

Trick 5: Const volatile modifiers

template <typename T> struct RemoveConst : type_is<T> {};
template <typename T> struct RemoveConst<T const> : type_is<T> {};
template <typename T> using RemoveConst_t = typename RemoveConst<T>::type;

template <typename T> struct RemoveVolatile : type_is<T> {};
template <typename T> struct RemoveVolatile<T volatile> : type_is<T> {};
template <typename T> using RemoveVolatile_t = typename RemoveVolatile<T>::type;

template <typename T>
struct RemoveCV : type_is<RemoveVolatile_t<RemoveConst_t<T>>> {};
template <typename T> using RemoveCV_t = typename RemoveCV<T>::type;

Given a type, we can remove its cv qualifiers. Follow the same logic, we can add cv qualifers to given type, which can be implemented by Readers if interested in.

Trick 6: Compile-time decision making

template <bool b, typename T, typename _> struct IF : type_is<T> {};
template <typename T, typename F> struct IF<false, T, F> : type_is<F> {};

template <bool b, typename T = void> struct enable_if : type_is<T> {};
template <typename T> struct enable_if<false, T> {};

The IF template means if given bool is true, it will return T, otherwise return F, by IF<>::type. The enable_if means if given bool is true, it will return the given type or default void, otherwise causes Substitution Failure in overloading set or compile error.

Trick 7: Template with Package template arguments

template <typename T, typename... P0toN> struct is_one_of;
template <typename T> struct is_one_of<T> : false_t {};
template <typename T, typename... Tail>
struct is_one_of<T, T, Tail...> : true_t {};
template <typename T, typename H, typename... Tail>
struct is_one_of<T, H, Tail...> : is_one_of<T, Tail...> {};

The technique to deal with package arguments is to treat the package arguments as a list of arguments, which you can get one element as the head of list at a time, and the remaining list as tail, then recursively get the head of the list and do something one the head.

With is_one_of template on our hand, we can revise the is_same template as following:

template <typename T, typename U> using is_same = is_one_of<T, U>;

And to check a given type if is void.

template <typename T> using is_void = is_same<RemoveCV_t<T>, void>;

Trick 8: Use built-in expressions that unevaluate their operands

There are some built-in expressions that unevaluate their operands, namely sizeof, typeid, decltype, noexcept.

Those expressions only look the declaration of their operands and get the type information for use. Therefore we can implement our own declval that return a rvalue of any given type. And check the well-formed-ness of arbitrary expression.

template <typename T> using add_rr_t = T &&;
template <typename T> add_rr_t<T> declval() noexcept;

template <typename T> struct is_copy_assignable {
private:
  template <typename U, typename = decltype(
                            declval<U &>() = declval<U const &>())>
  static true_t try_assignment(U &&);
  static false_t try_assignment(...);

public:
  using type = decltype(try_assignment(std::declval<T>()));
};

The decltype(declval<U&>()=declval<U const&>()) is to check the expression of copy assignment of Type U.

Trick 9: The expression as template argument must be well-formed

The rule that the expression as template argument must be well-formed can be used to well-formed-ness of expression as well.

template <typename...> using void_t = void;

template <typename T>
using copy_assignment_t = decltype(declval<T &>() = declval<T const &>());

template <typename T, typename = void> struct is_copy_assignable : false_t {};

template <typename T>
struct is_copy_assignable<T, void_t<copy_assignment_t<T>>>
    : is_same<copy_assignment_t<T>, T &> {};

template <typename, typename = void> struct has_type_member : false_t {};
template <typename T>
struct has_type_member<T, void_t<typename T::type>> : true_t {};

The void_t template means give me any type argument, I will give you a void back. But the inner process ensures that any expression as type argument must be well-formed, otherwise will cause substitution failure in template instantiation or compile error.

There we can rewrite is_copy_assignment template as showing above.

The Machinaries:

The major Machinary for compile-time computaion is Template, its template specialization as pattern match, its template instantiation as choosing the desired specialization with SFINAE mechanism.

In addition, function overloading, class inheritance, static constants, constant expression, built-in operand-unevaluation expression, type alias, template recursively instantiation, template lazy instantiation, and so on,  also play a role in compile-time evaluation.

One more about the Template of C++, the template system in C++ is turing complete, that means you can implement any algorithm in it with immutability.

Final words:

If you are interested in TMP of C++, you can check out the MPL library of Boost.