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 thedetector
struct used in the TSv2 detection idiom. In a nutshell, thedetector
attempts to instantiateaddition_test
with template argumentsT...
, if it fails it'll have a member type alias forstd::false_type
, otherwise it'll have a member type alias forstd::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 forstd::true_type
if all my requirements are satisfied, or an alias forstd::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.