Emulating template constraints in C++14

249 Views Asked by At

I'm coding in C++14 a program that uses template classes and functions. I don't like the compiler error messages I get when the template types don't fulfill the requirements I ask for with std::enable_if

I wondered if I could do better trying to emulate some functionality of C++20 concepts. Here are a few ideas I had to define a "concept" that checks the validity of an arbitrary number of expressions given some template parameters

  • say I wanted to check if types T and U support the addition operator, I'd write a test using a template alias like this
    template <typename T, typename U = T>
    using addition_test = decltype(declval<T>() + declval<U>());
  • Then I'd define a simple requirement like this
    template <typename ...T> 
    using addition_req = is_detected<addition_test, T...> 
  • The is_detected is an alias for a member type of the detector struct used in the TSv2 detection idiom. In a nutshell, the detector attempts to instantiate addition_test with template arguments T..., if it fails it'll have a member type alias for std::false_type, otherwise it'll have a member type alias for std::true_type.

  • I define my concept using the conjunction logical operations on traits introduced in c++17. The addition_group_concept below will be an alias for std::true_type if all my requirements are satisfied, or an alias for std::false_type otherwise.

template <typename T, typename U = T>
using addition_group_concept = conjunction <
        addition_req<T, U>,
        subtraction_req<T, U>,
        unary_plus_req<T>,
        unary_minus_req<T>
>;
  • Finally, I can use my "concept", like this
template <typename T, typename U>
auto add (T const & t, U const & u) ->
std::enable_if_t<addition_group_concept<T, U>::value, decltype(t + u)>
{
    return t + u;
}

template <typename T, typename U>
auto add (T const & t, U const & u) ->
std::enable_if_t<!addition_group_concept<T, U>::value, void>
{
    static_assert(always_false<T>::value,
                                "The operands must support addition and substraction operations.");
}

If my concept is not satisfied, I will get a compiler error message that is somewhat shorter and less cryptic than your usual failed template substitution message. So far so good (?)... but is far too much boilerplate code

I thought perhaps I could write a dummy function with a trailing return type, and put my requirements sequence in an std::enable_if in the trailing return type. Then I could define my concept with the help of template alias that is used as an adapter. Like this

// void_t is the c++17 void_t defined as:
// template <typename ...T>
// using void_t = void;

    template <typename ...Requirements>
    using requirement_seq = std::enable_if_t<conjunction<Requirements...>::value>;

    template <typename T>
    using simple_req = std::is_same<void_t<T>, void>;

    template <typename T, typename U = T>
    auto addition_group (T t, U u) -> requirement_seq<
        simple_req<decltype(t + u)>,
        simple_req<decltype(t - u)>,
        simple_req<decltype(+t)>,
        simple_req<decltype(-t)>,
        simple_req<decltype(+u)>,
        simple_req<decltype(-u)>
        >;

    template <typename ...T>
    using addition_group_adapter = decltype(addition_group<T...>);

    template <typename T, typename U = T>
    using addition_group_concept = is_detected<addition_group_adapter, T, U>;

And this works (!). And it's also the alternative that most closely matches the syntax of C++20 concepts. It could even be streamlined using a macro

#define SIMPLE_REQ(expression)\
    std::is_same<void_t<decltype(expression)>,void>

template <typename T, typename U = T>
auto addition_group (T t, U u) -> requirement_seq
    <
    SIMPLE_REQ(t + u),
    SIMPLE_REQ(t - u),
    SIMPLE_REQ(+t),
    SIMPLE_REQ(-t),
    SIMPLE_REQ(+u),
    SIMPLE_REQ(-u)
    >;

I wonder what people's opinions here are regarding this approach. What could be the potential shortcomings? Could this be done better or more succinctly?

I'd also love to have "granular" compiler error messages. At the moment, the error message I get is the one I write inside the static_assert of my add function, which doesn't tell me precisely which requirement failed in my requirement sequence.

0

There are 0 best solutions below