Combining void_t and enable_if?
Asked Answered
P

2

5

In C++17, void_t allow to easily do SFINAE with class/struct templates:

template <class T, class = void>
struct test {
    static constexpr auto text = "general case";
};

template <class T>
struct test<T, std::void_t<decltype(std::begin(std::declval<T>())>> {
    static constexpr auto text = "has begin iterator";
};

What's inside void_t is a type. My question is: how to do the same, when what's inside void_t is a type trait. Using enable_if works well:

template <class T>
struct test<T, std::void_t<std::enable_if_t<std::is_class_v<T>>> {
    static constexpr auto text = "is a class";
};

Is there a shorter/more elegant way to write this, or "the right way" to do it, is really to combine void_t and enable_if?

Pus answered 1/3, 2018 at 8:24 Comment(0)
G
4

A important point of std::void_t is that is variadic

// ................VVV  <- is variadic
template <typename ...>
using void_t = void;

so permit the SFINAE works when you have to check a multiplicity of types and permit you a soft fail when only one of them fail.

In a case when you have to check only a value and you have to check it with std::enable_if (or a similar type trait) I don't see reason to use it together with std::void_t.

So, in your example, "the right way" (IMHO) is avoid the use of std::void_t

template <class T>
struct test<T, std::enable_if_t<std::is_class_v<T>>
 { static constexpr auto text = "is a class"; };

Also in the case of the use of a single decltype() I prefer the old way (but I suppose it's a question of personal taste)

template <class T>
struct test<T, decltype(std::begin(std::declval<T>(), void())>
 { static constexpr auto text = "has begin iterator"; };
Geotectonic answered 1/3, 2018 at 10:22 Comment(2)
No, we have to use this awesome new technique. In fact, it's so awesome we should use it as many times as we can: std::void_t<std::void_t<std::void_t<std::void_t<std::void_t<std::enable_if_t<...>>>>>> :)Naaman
@Naaman - do you mean that I'm obsolete with old decltype() way to declare an integer value :( - decltype(decltype(decltype(decltype(decltype(std::enable_if_t<true>(), int{}){}){}){}){}) intVal;Geotectonic
N
10

You don't seem to understand why std::void_t is necessary. Let's fix that :)

In your first example, if you don't use std::void_t, then the partial specialization will never get chosen, because the decltype would evaluate to some type T that is not void, and so it won't be a match the partial specialization and would fall back to the general case. Now you can always change the default argument in the primary template if you know that your function will always return the same type, but that way is way more brittle to change. (You can have a look at the answer that I gave to another question which might help to understand this).

This is why std::void_t was introduced. std::void_t is just an identity type trait that is void no matter what. This means that in your first example, no matter what the decltype evaluates to, the second template parameter will be void and thus will match and the specialization will get chosen, if the decltype is well-formed.

std::enable_if_t is valid and is by default void if and only if the condition inside it evaluates to true. This means that std::enable_if_t already returns a void no matter what and thus you don't need std::void_t to "transform" it into a void, because it is already void.

Newberry answered 3/3, 2018 at 9:23 Comment(1)
If I "explained" something that doesn't make sense, please tell me. Sometimes I can't explain things very well, thanks :)Newberry
G
4

A important point of std::void_t is that is variadic

// ................VVV  <- is variadic
template <typename ...>
using void_t = void;

so permit the SFINAE works when you have to check a multiplicity of types and permit you a soft fail when only one of them fail.

In a case when you have to check only a value and you have to check it with std::enable_if (or a similar type trait) I don't see reason to use it together with std::void_t.

So, in your example, "the right way" (IMHO) is avoid the use of std::void_t

template <class T>
struct test<T, std::enable_if_t<std::is_class_v<T>>
 { static constexpr auto text = "is a class"; };

Also in the case of the use of a single decltype() I prefer the old way (but I suppose it's a question of personal taste)

template <class T>
struct test<T, decltype(std::begin(std::declval<T>(), void())>
 { static constexpr auto text = "has begin iterator"; };
Geotectonic answered 1/3, 2018 at 10:22 Comment(2)
No, we have to use this awesome new technique. In fact, it's so awesome we should use it as many times as we can: std::void_t<std::void_t<std::void_t<std::void_t<std::void_t<std::enable_if_t<...>>>>>> :)Naaman
@Naaman - do you mean that I'm obsolete with old decltype() way to declare an integer value :( - decltype(decltype(decltype(decltype(decltype(std::enable_if_t<true>(), int{}){}){}){}){}) intVal;Geotectonic

© 2022 - 2024 — McMap. All rights reserved.