Consider the following, basic example based on void_t
:
template<typename, typename = void_t<>>
struct S: std::false_type {};
template<typename T>
struct S<T, void_t<decltype(std::declval<T>().foo())>>: std::true_type {};
It can be used as it follows:
template<typename T>
std::enable_if_t<S<T>::value> func() { }
The same can be done using trailing return type and decltype
:
template<typename T>
auto func() -> decltype(std::declval<T>().foo(), void()) { }
This is true for all the examples I thought of. I failed in finding a case in which either void_t
or the trailing return type with decltype
can be used while its counterpart cannot.
The most complex cases can ever be resolved with a combination of trailing return type and overloading (as an example, when the detector is used to switch between two functions instead of as a trigger to disable or enable something).
Is this the case? Are they (void_t
and decltype
as trailing return type plus overloading if needed) completely interchangeable?
Otherwise, what's a case in which one cannot be used to work around the constraints and I'm forced to use a specific method?
This is the metaprogramming equivalent of: should I write a function or should I just write my code inline. The reasons to prefer to write a type trait are the same as the reasons to prefer to write a function: it's more self-documenting, it's reusable, it's easier to debug. The reasons to prefer to write trailing decltype are the similar to the reasons to prefer to write code inline: it's a one-off that isn't reusable, so why put in the effort factoring it out and coming up with a sensible name for it?
But here are a bunch of reasons why you might want a type trait:
Suppose I have a trait I want to check lots of times. Like fooable
. If I write the type trait once, I can treat that as a concept:
template <class, class = void>
struct fooable : std::false_type {};
template <class T>
struct fooable<T, void_t<decltype(std::declval<T>().foo())>>
: std::true_type {};
And now I can use that same concept in tons of places:
template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void bar(T ) { ... }
template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void quux(T ) { ... }
For concepts that check more than a single expression, you don't want to have to repeat it every time.
Going along with repetition, composing two different type traits is easy:
template <class T>
using fooable_and_barable = std::conjunction<fooable<T>, barable<T>>;
Composing two trailing return types requires writing out all of both expressions...
With a type trait, it's easy to check that a type doesn't satisfy a trait. That's just !fooable<T>::value
. You can't write a trailing-decltype
expression for checking that something is invalid. This might come up when you have two disjoint overloads:
template <class T, std::enable_if_t<fooable<T>::value>* = nullptr>
void bar(T ) { ... }
template <class T, std::enable_if_t<!fooable<T>::value>* = nullptr>
void bar(T ) { ... }
which leads nicely into...
Assuming we have a short type trait, it's a lot clearer to tag dispatch with a type trait:
template <class T> void bar(T , std::true_type fooable) { ... }
template <class T> void bar(T , std::false_type not_fooable) { ... }
template <class T> void bar(T v) { bar(v, fooable<T>{}); }
than it would be otherwise:
template <class T> auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
template <class T> void bar(T v, ... ) { ... }
template <class T> void bar(T v) { bar(v, 0); }
The 0
and int/...
is a little weird, right?
static_assert
What if I don't want to SFINAE on a concept, but rather just want to hard fail with a clear message?
template <class T>
struct requires_fooability {
static_assert(fooable<T>{}, "T must be fooable!");
};
When (if?) we ever get concepts, obviously actually using concepts is much more powerful when it comes to everything related to metaprogramming:
template <fooable T> void bar(T ) { ... }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With