Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

void_t and trailing return type with decltype: are they completely interchangeable?

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?

like image 287
skypjack Avatar asked Sep 17 '16 22:09

skypjack


1 Answers

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:

Repetition

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.

Composability

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...

Negation

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...

Tag dispatch

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!");
};

Concepts

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 ) { ... }
like image 151
Barry Avatar answered Nov 18 '22 01:11

Barry