Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enforcing invariants in metaprogramming

I'd like to be able to check invariants for classes that are used in metaprograms. My first naive approach was

template <int N>
struct digit
{
  static_assert((N >= 0) && (N < 10), "bad invariant");
};

using boom = digit<99>;

However this compiles without any problems. The static assertion is triggered only when the illegal class is constructed.

It is possible when adding an additional template parameter:

#include <type_traits>

template <int N, 
          typename = typename std::enable_if<(N >= 0) && (N < 10)>::type>
struct digit;

using crash = digit<-7>;

When I wanted to apply this technique to a class that is used as a list of types:

#include <type_traits>

template <typename ...> struct are_integral;

template <typename T, typename ...Ts>
struct are_integral<T, Ts...>
{
  static const bool value = std::is_integral<T>::value &&
                            are_integral<Ts...>::value;
};

template <>
struct are_integral<> : std::true_type { };



template <typename ...Ts,
          typename = typename std::enable_if<are_integral<Ts...>::value>::type>
struct list;

using ok = list<int, long, char>;
using bad = list<double>;

It simply does not work since gcc complains that

error: parameter pack 'Ts' must be at the end of the template parameter list struct list;

Even if it would work, the class is useless as the template parameter pack does not reflect the typelist.

So I tried to use an "illegal" base class:

template <typename> struct check;

template <typename ...Ts>
struct list : check<typename std::enable_if<are_integral<Ts...>::value>::type>
{ };

using ok = list<int, long, char>;
using bad = list<double>;

This compiles without problems.

Is there any way to accomplish something like that in c++11 or do I have to wait for concepts?

like image 951
MadScientist Avatar asked Oct 31 '22 00:10

MadScientist


1 Answers

Your problem arises because the template is not instantiated when it is aliased, so the static_assert does not trigger.

If this is acceptable, you could add some indirection and use a builder metafunction to create your compile-time list. This metafunction would perform the check.

template <typename ...Ts>
struct make_list
{ 
    static_assert(are_integral<Ts...>::value, "all types must be integral");
    typedef list<Ts...> type;
};

using ok = make_list<int, long, char>::type;
using bad = make_list<double>::type;

Another solution is to use a dummy type to wrap your parameter pack into a first-class type.

// dummy wrapper
template <typename ...>
struct pack;

template <typename ...> struct are_integral;

// specialization for wrapped packs
template <typename ...Ts>
struct are_integral<pack<Ts...>> : are_integral<Ts...>
{
};

template <typename T, typename ...Ts>
struct are_integral<T, Ts...>
{
  static const bool value = std::is_integral<T>::value &&
                            are_integral<Ts...>::value;
};

template <>
struct are_integral<> : std::true_type { };

// helper type which performs the check
template <typename Pack,
          typename = typename std::enable_if<are_integral<Pack>::value>::type>
struct list_helper;

// actual type (alias) you will expose
template <typename ...Ts>
using list = list_helper<pack<Ts...>>;

using ok = list<int, long, char>;
using bad = list<double>; // compiler error

Using a wrapper often comes in handy when dealing with parameter packs, because it makes them more amenable to manipulation: the wrapper is a type like any other, which can be stored, appear anywhere in the parameter list, be passed to unary metafunctions, etc.

like image 94
Luc Touraille Avatar answered Nov 08 '22 07:11

Luc Touraille