How Does 'Void_T' Work

How does `void_t` work

1. Primary Class Template

When you write has_member<A>::value, the compiler looks up the name has_member and finds the primary class template, that is, this declaration:

template< class , class = void >
struct has_member;

(In the OP, that's written as a definition.)

The template argument list <A> is compared to the template parameter list of this primary template. Since the primary template has two parameters, but you only supplied one, the remaining parameter is defaulted to the default template argument: void. It's as if you had written has_member<A, void>::value.

2. Specialized Class Template

Now, the template parameter list is compared against any specializations of the template has_member. Only if no specialization matches, the definition of the primary template is used as a fall-back. So the partial specialization is taken into account:

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

The compiler tries to match the template arguments A, void with the patterns defined in the partial specialization: T and void_t<..> one by one. First, template argument deduction is performed. The partial specialization above is still a template with template-parameters that need to be "filled" by arguments.

The first pattern T, allows the compiler to deduce the template-parameter T. This is a trivial deduction, but consider a pattern like T const&, where we could still deduce T. For the pattern T and the template argument A, we deduce T to be A.

In the second pattern void_t< decltype( T::member ) >, the template-parameter T appears in a context where it cannot be deduced from any template argument.

There are two reasons for this:

  • The expression inside decltype is explicitly excluded from template argument deduction. I guess this is because it can be arbitrarily complex.

  • Even if we used a pattern without decltype like void_t< T >, then the deduction of T happens on the resolved alias template. That is, we resolve the alias template and later try to deduce the type T from the resulting pattern. The resulting pattern, however, is void, which is not dependent on T and therefore does not allow us to find a specific type for T. This is similar to the mathematical problem of trying to invert a constant function (in the mathematical sense of those terms).

Template argument deduction is finished(*), now the deduced template arguments are substituted. This creates a specialization that looks like this:

template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };

The type void_t< decltype( A::member ) > can now be evaluated. It is well-formed after substitution, hence, no Substitution Failure occurs. We get:

template<>
struct has_member<A, void> : true_type
{ };

3. Choice

Now, we can compare the template parameter list of this specialization with the template arguments supplied to the original has_member<A>::value. Both types match exactly, so this partial specialization is chosen.


On the other hand, when we define the template as:

template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

We end up with the same specialization:

template<>
struct has_member<A, void> : true_type
{ };

but our template argument list for has_member<A>::value now is <A, int>. The arguments do not match the parameters of the specialization, and the primary template is chosen as a fall-back.


(*) The Standard, IMHO confusingly, includes the substitution process and the matching of explicitly specified template arguments in the template argument deduction process. For example (post-N4296) [temp.class.spec.match]/2:

A partial specialization matches a given actual template argument list
if the template arguments of the partial specialization can be deduced
from the actual template argument list.

But this does not just mean that all template-parameters of the partial specialization have to be deduced; it also means that substitution must succeed and (as it seems?) the template arguments have to match the (substituted) template parameters of the partial specialization. Note that I'm not completely aware of where the Standard specifies the comparison between the substituted argument list and the supplied argument list.

why is void_t necessary to check for the existence of a member type?

In those lines:

template <typename X, typename Y = X>
struct has_rebind : std::false_type {};

template <typename X>
struct has_rebind<X, typename X::template rebind<int>> : std::true_type {};

the default type (X) and the one you provided for specialization (typename X::template rebind<int>) must be the same type. Since void is a pretty good "default dummy type", it's often used as the default type and we use void_t to change whatever was given in the specialization

Why does std::void_t not work in such a case?

std::iterator_traits<T>::iterator_category forces instantiation of std::iterator_traits<T> which is ill formed (hard error and not soft error for SFINAE) for void*.

You have to handle void* manually.

Why void_t doesnt work in SFINAE but enable_if does

This seems to be related to CWG issue #1980 (credits to T.C. for correcting me).

As a workaround you can define void_t as:

template<typename... Ts> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;

(from cppreference)

live example on wandbox

C++ SFINAE void_t not working

std::declval<T> has return type T&&, so std::declval<int>() is an rvalue of type int. The result of "false" is telling you that an int rvalue is not incrementable, which is correct.

You can replace std::declval<T> with std::declval<T&> to get the program to tell you whether an lvalue of type T is incrementable. If you make this change to your program, it should print "true".

Combining void_t and enable_if?

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

Why does the void_t detection idiom not work with gcc-4.9? [duplicate]

This comes about as a result of CWG Issue 1558, and is now considered a bug in gcc (specifically 64395 - currently fixed). The idea behind the issue is that since you don't actually use the template parameters here:

template <typename...> using void_t = void;

there's no substitution failure regardless of what types or expressions you try to pass in.

Thankfully, there's an easy workaround that doesn't involve upgrading your compiler. We can rewrite void_t to actually use its parameter pack, thereby triggering the substitution failure:

namespace void_details {
template <class... >
struct make_void { using type = void; };
}

template <class... T> using void_t = typename void_details ::make_void<T...>::type;

That'll make your example do the right thing across all the gcc versions I tried.

sample void_t example does not compile with intel compiler 19.0.4

The problem is not related to a compiler but to the implementation of the C++ Standard Library it works with. For instance, on Linux systems, Intel icpc typically uses system-installed libstdc++. Note that the support for std::void_t was added into libstdc++ in version 6.1.

While icpc on Godbold uses libstdc++ version 8, your system likely have some older version installed without the support for std::void_t.



Related Topics



Leave a reply



Submit