ChatGPT解决这个技术问题 Extra ChatGPT

How does `void_t` work

I watched Walter Brown's talk at Cppcon14 about modern template programming (Part I, Part II) where he presented his void_t SFINAE technique.

Example:
Given a simple variable template that evaluates to void if all template arguments are well formed:

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

and the following trait that checks for the existence of a member variable called member:

template< class , class = void >
struct has_member : std::false_type
{ };

// specialized as has_member< T , void > or discarded (sfinae)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };

I tried to understand why and how this works. Therefore a tiny example:

class A {
public:
    int member;
};

class B {
};

static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );

1. has_member< A >

has_member< A , void_t< decltype( A::member ) > > A::member exists decltype( A::member ) is well-formed void_t<> is valid and evaluates to void

A::member exists

decltype( A::member ) is well-formed

void_t<> is valid and evaluates to void

has_member< A , void > and therefore it chooses the specialized template

has_member< T , void > and evaluates to true_type

2. has_member< B >

has_member< B , void_t< decltype( B::member ) > > B::member does not exist decltype( B::member ) is ill-formed and fails silently (sfinae) has_member< B , expression-sfinae > so this template is discarded

B::member does not exist

decltype( B::member ) is ill-formed and fails silently (sfinae)

has_member< B , expression-sfinae > so this template is discarded

compiler finds has_member< B , class = void > with void as default argument

has_member< B > evaluates to false_type

http://ideone.com/HCTlBb

Questions:
1. Is my understanding of this correct?
2. Walter Brown states that the default argument has to be the exact same type as the one used in void_t for it to work. Why is that? (I don't see why this types need to match, doesn't just any default type does the job?)

Ad 2) Imagine the static assert was written as: has_member<A,int>::value. Then, the partial specialization that evaluates to has_member<A,void> cannot match. Therefore, it needs to be has_member<A,void>::value, or, with syntactic sugar, a default argument of type void.
@dyp Thanks, I'll edit that. Mh, I do not see a need in having has_member< T , class = void > defaulting in void yet. Assuming this trait will be used only with 1 template argument at any time, then the default argument could be any type?
Interesting question.
Note that In this proposal, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf, Walter changed template <class, class = void> to template <class, class = void_t<>>. So now we are free to do whatever we want with void_t alias template implementation :)

A
Arsen Khachaturyan

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.


Thank you! I've read it over and over again, and I guess my thinking of how template argument deduction exactly works and what the compiler chooses for the final template is at the moment not correct.
@JohannesSchaub-litb Thanks! That's a bit depressing, though. Are there really no rules for matching a template argument with a specialization? Not even for explicit specializations?
W/r/t default template arguments, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
@dyp A few weeks later and reading alot about this and with a hint from this snippet i think i begin to understand how this works. Your explanation makes from read to read more sense to me, thanks!
I wanted to add, that the term primary template was the key (the templates first encounter in the code)
J
Jarod42
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

That above specialization exists only when it is well formed, so when decltype( T::member ) is valid and not ambiguous. the specialization is so for has_member<T , void> as state in the comment.

When you write has_member<A>, it is has_member<A, void> because of default template argument.

And we have specialization for has_member<A, void> (so inherit from true_type) but we don't have specialization for has_member<B, void> (so we use the default definition : inherit from false_type)


So void_t<decltype(T::member)>> is nothing more than a standardized/clearer way of writing decltype(T::member, void())?
@303: some (old) compiler version had difficulty to apply SFINAE with (some implementations of) void_t though.
u
user3059627

This thread and the thread SFINAE: Understanding void_t and detect_if saved me. I want to demonstrate the behavior by some examples:

The tool: cppinsights

To test the implmentation by types of float and following types:

struct A {
    using type = int;
};

struct B{
    using type = void;
}

Tested by

auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;

standard implmentataion

From this std::void_t reference

#include <type_traits>

// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };

// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { }; 

Output

bool f = false;
bool a = true;
bool b = true;

bool x = has_type_member<A, int>::value; //x = false;

case 1

#include <type_traits>

// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };

template< class T >
struct has_type_member<T, void> : std::true_type { };

// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { }; 

Output

/home/insights/insights.cpp:14:8: error: redefinition of 'has_type_member<T, std::void_t<typename T::type>>'
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/insights/insights.cpp:8:8: note: previous definition is here
struct has_type_member<T, void>: std::true_type {};
       ^
1 error generated.
Error while processing /home/insights/insights.cpp.

So, has_type_member<T, std::void_t<typename T::type>> defined a specialization of has_type_member and the signature is exactly has_type_member<T, void>.

case 2

#include <type_traits>

template< class, class = void >
struct has_type_member : std::false_type { };

// specialize 2nd type as void
template< class T>
struct has_type_member<T, void> : std::true_type { };

Output:

bool f = true;
bool a = true;
bool b = true;

This case shows that the compiler:

Wanted to find a match for has_type_member Found out that the template requires 2 arguments, then filled the 2nd argument by default arguments. The struct was like has_type_member Found a specialization of this signature and got the value from std::true_type

case 3

#include <type_traits>

template< class, class = void >
struct has_type_member : std::false_type { };

template<class T>
struct has_type_member<T, typename T::type>: std::true_type {}; 

Output:

bool f = false;
bool a = false;
bool b = true;

case f

has_type_member was completed into has_type_member. Then the compiler tried typename float::type and failed. The primary template picked.

case a

has_type_member was completed into has_type_member Then compiler tried has_type_member and found out it was has_type_member Compiler decided it was not a specilization of has_type_member Then primary template picked.

case b

has_type_member was completed into has_type_member. Then compiler tried has_type_member and found out it was has_type_member. Compiler decided it was a specilization of has_type_member true_type picked.

case 4

#include <type_traits>

//int as default 2nd argument
template< class, class = int >
struct has_type_member : std::false_type { };

template<class T>
struct has_type_member<T, std::void<typename T::type>>: std::true_type {}; 

Output:

bool f = false;
bool a = false;
bool b = false;

The has_type_member<T> is of type has_type_member<T, int> for all 3 variables, while the true_type has signature as has_type_member<T, void> if it's valid.

Conclusion

So, the std::void_t:

Check if T::type valid. Provides a specialization of primary template if only one template argument provided.