checking expression validity in-place with C++17
When writing generic code, it is sometimes useful to check whether or not a particular SFINAE-friendly expression is valid (e.g. to branch at compile-time). Let’s assume that we have the following class declarations…
struct Cat
{
void meow() const { cout << "meow\n"; }
};
struct Dog
{
void bark() const { cout << "bark\n"; }
};
…and that we would like to write a template function make_noise(x)
that calls x.meow()
and/or x.bark()
if they are well-formed expressions:
template <typename T>
void make_noise(const T& x)
{
// Pseudocode:
/*
if(`x.meow()` is well-formed)
{
execute `x.meow();`
}
else if(`x.bark()` is well-formed)
{
execute `x.bark();`
}
else
{
compile-time error
}
*/
}
In this article I’ll show how to implement the pseudocode in:
C++11: using
std::void_t
andstd::enable_if
.C++14: using
boost::hana::is_valid
andvrm::core::static_if
.C++17: using
if constexpr(...)
,constexpr
lambdas, and std::is_callable. This version will allow expression validity to be checked in-place (i.e. directly in theif constexpr
predicate). Variadic preprocessor macros will also be used to make the user code easier to read and maintain.
(Note: if you are familiar with C++11 and C++14 techniques for expression validity detection, you can directly skip to the in-place C++17 detection technique.)
C++11 implementation
The technique that will be used in the C++11 implementation consists of combining std::void_t
with std::enable_if
- this allows us to detect ill-formed expressions in SFINAE contexts. I first heard about this method by attending the excellent “Modern Template Metaprogramming: A Compendium” talk by Walter E. Brown at CppCon 2014 - I remember being mind-blown after the talk!
Let’s begin by implementing void_t
, which is only part of the standard since C++17:
(Note: as /u/bluescarni mentioned on reddit, you may need a workaround when compiling in C++11 mode due to CWG 1558.)
Yeah, it’s that simple. In order to use void_t
to detect expression validity, partial template specialization must be used - here’s a detector class for meow
:
template <typename, typename = void>
struct has_meow : std::false_type { };
template <typename T>
struct has_meow<T, void_t<decltype(std::declval<T>().meow())>>
: std::true_type { };
The way this idiom works is not very complex: roughly speaking, instantiating has_meow<T>
will attempt to evaluate void_t<decltype(std::declval<T>().meow())>
.
If
declval<T>().meow()
is well-formed,void_t<decltype(declval<T>().meow())>>
will evaluate tovoid
, andhas_meow
’sstd::true_type
specialization will be taken.If
declval<T>().meow()
is ill-formed,void_t<decltype(declval<T>().meow())>>
will be ill-formed as well, thus removing thestd::true_type
specialization thanks to SFINAE - all that’s left is thestd::false_type
specialization.
std::declval<T>()
is being used in place of T{}
because it is not guaranteed that T
is default-constructible.
After defining the has_bark
detector class (which is trivial to implement, as well), all that’s left to do is use std::enable_if
to constrain make_noise
:
template <typename T>
auto make_noise(const T& x)
-> typename std::enable_if<has_meow<T>{}>::type
{
x.meow();
}
template <typename T>
auto make_noise(const T& x)
-> typename std::enable_if<has_bark<T>{}>::type
{
x.bark();
}
That’s it for C++11! You can find a complete example on GitHub.
(Note: for more void_t
goodness, check out the detection idiom.)
C++14 implementation
There are some annoyances in the C++11 implementation:
A detector class has to be defined for every expression we want to check. The class cannot be defined locally.
std::enable_if
has to be used to constrain multiple versions of the same function. It is not possible to “branch” locally at compile-time.
Both those issues can be solved thanks to one of my favorite features introduced in C++14: generic lambdas.
Since generic lambdas are “templates in disguise”, they provide a SFINAE-friendly context. Therefore, we can create an is_valid
function that allows us to take advantage of the previously seen void_t
detection idiom without explicitly having to create a new struct
:
auto has_meow = is_valid([](auto&& x) -> decltype(x.meow()){});
static_assert(has_meow(Cat{}), "");
static_assert(!has_bark(Cat{}), "");
As you can see, has_meow
can be locally instantiated in any scope, and can be used to check expression validity with a nicer syntax.
How does it work?
It’s not complicated - is_valid
is a simple constexpr
function that takes a callable object of type TF
and returns a validity_checker<TF>
instance.
The validity_checker
class’s operator()
is a constexpr
variadic template that checks whether or not TF
is callable with the given arguments.
template <typename TF>
struct validity_checker
{
template <typename... Ts>
constexpr auto operator()(Ts&&...) const
{
return is_callable<TF(Ts...)>{};
}
};
Finally, is_callable
is a type-trait-like class that can be easily implemented using void_t
. It evaluates to std::true_type
if the passed signature would result in a well-formed function invocation.
template <typename, typename = void>
struct is_callable : std::false_type { };
template <typename TF, class... Ts>
struct is_callable<TF(Ts...),
void_t<decltype(std::declval<TF>()(std::declval<Ts>()...))>>
: std::true_type { };
This solves the first C++11 annoyance, by allowing us to instantiate detectors locally. The second issue is not as easy to straighten out - branching locally at compile-time would require something like if constexpr(...)
(a.k.a. static_if
), which is only available in C++17…
…but it is actually possible to implement a working static_if
in C++14, albeit with a slightly cumbersome syntax. I explain how in my CppCon 2016 talk: “Implementing static
control flow in C++14”.
Once we have that, we can finally implement our make_noise
function:
template <typename T>
auto make_noise(const T& x)
{
auto has_meow = is_valid([](auto&& x) -> decltype(x.meow()){ });
auto has_bark = is_valid([](auto&& x) -> decltype(x.bark()){ });
static_if(has_meow(x))
.then([](auto&& y)
{
y.meow();
})
.else_if(has_bark(x))
.then([](auto&& y)
{
y.bark();
})
.else_([](auto&&)
{
// The pattern below generates a compiler-error.
// It is not possible to use `static_assert(false)`
// here, as it triggers whether or not the branch
// is taken.
struct cannot_meow_or_bark;
cannot_meow_or_bark{};
})(x);
}
You can find a complete example on GitHub.
Is this a better implementation compared to the C++11 version? That’s debatable. There are, however, some objective advantages:
Expression validity detector definition/instantiation is local to the function scope.
There is a single overload of
make_noise
- compile-time branching is local to the function scope.
These advantages become more important when nesting multiple static_if
blocks together and dealing with more complicated validity checking: the equivalent C++11 code would require an huge amount of boilerplate and std::enable_if
constraints compared to the C++14 implementation.
(Note: boost::hana:is_valid
is a production-ready C++14 implementation of the above is_valid
function.)
(Note: you can find my static_if
implementation in vrm::core::static_if
.)
C++17 implementation
The previous implementation took care of C++11’s annoyances, but introduced some new ones:
is_valid
has to be assigned to a variable in order to be used in a constant expression. This happens because lambdas are notconstexpr
.Verbosity. Having to use something like
static_if
makes the code much less readable. Having to create a lambda with adecltype(...)
trailing return type for every expression creates noise.
We can solve both these annoyances thanks to some new features introduced in C++17 and to some macro black magic. The final result will look like this:
template <typename T>
auto make_noise(const T& x)
{
if constexpr(IS_VALID(T)(_0.meow()))
{
x.meow();
}
else if constexpr(IS_VALID(T)(_0.bark()))
{
x.bark();
}
else
{
struct cannot_meow_or_bark;
cannot_meow_or_bark{};
}
}
Before diving into the implementation, let’s analyze the user syntax. IS_VALID
is a variadic macro that takes any number of types and “returns” another variadic macro that takes an expression built using some type placeholders (e.g. _0
, _1
, …). The combination of the two macros is a constant expression that evaluates to true
if the expression is valid for the given types, to false
otherwise. Here are some other example invocations:
// Can `T` be dereferenced?
IS_VALID(T)(*_0);
// Can `T0` and `T1` be added together?
IS_VALID(T0, T1)(_0 + _1);
// Can `T` be streamed into itself?
IS_VALID(T)(_0 << _0);
// Can a tuple be made out of `T0`, `T1` and `float`?
IS_VALID(T0, T1, float)(std::make_tuple(_0, _1, _2));
All the IS_VALID
invocations shown above can be used in contexts where only a constant expression is accepted such as static_assert(...)
or if constexpr(...)
.
What is this magic!?
Time to reveal the dark secrets of IS_VALID
. Let’s begin by defining some utilities that will allow types to be wrapped into values.
template <typename T>
struct type_w
{
using type = T;
};
template <typename T>
constexpr type_w<T> type_c{};
Types can now be wrapped into values like this: type_c<int>
. The type inside a type_c
wrapped can be retrieved as follows:
(Note: the idea of wrapping types into values (and viceversa) is a core principle of boost::hana
.)
After that, a new implementation of the previously seen validity_checker
that works with type_c
is required:
template <typename TF>
struct validity_checker
{
template <typename... Ts>
constexpr auto operator()(Ts... ts)
{
return std::is_callable<
TF(typename decltype(ts)::type...)
>{};
}
};
template <typename TF>
constexpr auto is_valid(TF)
{
return validity_checker<TF>{};
}
(Note: std::is_callable
is equivalent to the previously seen is_callable
and is part of the C++17 standard.)
This validity_checker
is conceptually equivalent to the previous one, but it expects ts...
to be a pack of type_c
instances which are automatically unwrapped in the std::is_callable
instantiation.
Before moving onto the inner workings of IS_VALID
, let’s see how constexpr
lambdas (standardized in C++17) can be used to evaluate is_valid
in-place inside a constant expression.
// Make sure that `int*` can be dereferenced.
static_assert(
is_valid([](auto _0) constexpr -> decltype(*_0){})
(type_c<int*>)
);
Yikes. This works and compiles, but it’s verbose and full of noise/boilerplate. That’s why a macro *shudders* is needed here. Let’s finally check out how IS_VALID
is implemented. (For simplicity, only the single-type version will be analyzed. A fully-variadic IS_VALID
is simple to implement - see the example on GitHub, which uses my vrm_pp
preprocessor metaprogramming library.)
template <typename T, typename TF>
constexpr auto operator|(T x, validity_checker<TF> vc)
{
return std::apply(vc, x);
}
#define IS_VALID_EXPANDER(...) \
is_valid([](auto _0) constexpr->decltype(__VA_ARGS__){})
#define IS_VALID(type0) \
std::make_tuple(type_c<type0>) | IS_VALID_EXPANDER
In order to understand this madness, let’s use an example:
Let’s expand IS_VALID
:
Let’s expand IS_VALID_EXPANDER
:
The is_valid(...)
call evaluates to a validity_checker<...>
instance. As it is not possible to explicitly name the type of the lambda, we’ll refer to this particular instance as some_validity_checker
in the following examples.
The operator|(T, validity_checker<...>)
overload can now be evaluated:
std::apply(std::make_tuple(type_c<int*>), some_validity_checker);
// ...which is equivalent to...
some_validity_checker(type_c<int*>)
(Note: std::apply
invokes a callable object by “unpacking” the contents of a tuple as its arguments. It was introduced in C++17.)
Finally, some_validity_checker(type_c<int*>)
is a constant expression that evaluates to either true
or false
.
The std::tuple
and the operator|
overload are there just to make the IS_VALID(types...)(expression)
syntax possible. Alternatively, the user would have had to specify the number of types as part of the macro name itself. Separating the expression from the types allows variadic macro argument counting techniques to be easily applied.
That’s it! You can find a complete example on GitHub.
I think this technique is very useful when combined with if constexpr(...)
- it’s a barebones “in-place concept” definition and check. Example:
template <typename T0, typename T1>
auto some_generic_function(T0 a, T1 b)
{
if constexpr(IS_VALID(T0, T1)(foo(_0, _1))
{
return foo(a, b);
}
else if constexpr(IS_VALID(T0, T1)(_0 + _1))
{
return a + b;
}
// ...
}
…there’s a small temporary caveat, however: neither g++
nor clang++
’s latest versions can currently compile IS_VALID
inside an if constexpr(...)
branch which is part of a template context:
clang++
hasn’t implemented support forconstexpr
lambdas yet.g++
has, but there’s a bug I found and reported (as #78131).
IS_VALID
does work properly with g++
trunk in other contexts where a constant expression is required though (e.g. non-template context if constexpr(...)
and static_assert
).
Addendum
This section was written on 04/02/2017.
Major simplification
When I woke up today I was extremely happy to see that Fabio managed to simplify IS_VALID
’s implementation significantly. He posted his work in the comments and sent a PR that I accepted. Thanks - very appreciated!
I decided to cover his improvements here. Readers interested in implementing IS_VALID
should definitely use his simplified version.
Avoid using
type_w
- it was not necessary after all!validity_checker
can be defined as:template <typename ...Ts> struct validity_checker { template <typename TF> static constexpr auto is_valid(TF) { return std::is_callable<std::decay_t<TF>(Ts...)>{}; } };
As an example, for one argument,
IS_VALID
can be expanded to:Generating variadic expansions of
IS_VALID
on the fly using myvrm_pp
preprocessor metaprogramming library.#define IS_VALID_EXPANDER_BEGIN(count) \ is_valid([](VRM_PP_REPEAT_INC(count, IS_VALID_EXPANDER_MIDDLE,_)) \ constexpr -> decltype IS_VALID_EXPANDER_END #define IS_VALID_EXPANDER_MIDDLE(idx, _) \ VRM_PP_COMMA_IF(idx) auto _##idx #define IS_VALID_EXPANDER_END(...) (__VA_ARGS__){}) #define IS_VALID(...) \ validity_checker<__VA_ARGS__>:: \ IS_VALID_EXPANDER_BEGIN(VRM_PP_ARGCOUNT(__VA_ARGS__))
VRM_PP_REPEAT_INC(count, IS_VALID_EXPANDER_MIDDLE,_)
is used to generate theauto _0, auto_1, /*...*/
arguments.IS_VALID_EXPANDER_BEGIN(VRM_PP_ARGCOUNT(__VA_ARGS__))
is used to count the number of types passed toIS_VALID
, and to begin generating the expansion.