4

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?

mutableVoid
  • 1,284
  • 2
  • 11
  • 29
Vincent
  • 57,703
  • 61
  • 205
  • 388

2 Answers2

9

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.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • 3
    If I "explained" something that doesn't make sense, please tell me. Sometimes I can't explain things very well, thanks :) – Rakete1111 Mar 03 '18 at 09:24
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"; };
max66
  • 65,235
  • 10
  • 71
  • 111
  • 6
    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>>>>>` :) – T.C. Mar 01 '18 at 10:55
  • 2
    @T.C. - 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(), int{}){}){}){}){}) intVal;` – max66 Mar 01 '18 at 12:28