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
existsdecltype( A::member )
is well-formedvoid_t<>
is valid and evaluates tovoid
has_member< A , void >
and therefore it chooses the specialized templatehas_member< T , void >
and evaluates totrue_type
2. has_member< B >
has_member< B , void_t< decltype( B::member ) > >
B::member
does not existdecltype( 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 tofalse_type
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?)
When you write
has_member<A>::value
, the compiler looks up the namehas_member
and finds the primary class template, that is, this declaration:(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 writtenhas_member<A, void>::value
.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:The compiler tries to match the template arguments
A, void
with the patterns defined in the partial specialization:T
andvoid_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-parameterT
. This is a trivial deduction, but consider a pattern likeT const&
, where we could still deduceT
. For the patternT
and the template argumentA
, we deduceT
to beA
.In the second pattern
void_t< decltype( T::member ) >
, the template-parameterT
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
likevoid_t< T >
, then the deduction ofT
happens on the resolved alias template. That is, we resolve the alias template and then try to deduce the typeT
from the resulting pattern. The resulting pattern however isvoid
, which is not dependent onT
and therefore does not allow us to find a specific type forT
. 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:
The type
void_t< decltype( A::member ) > >
can now be evaluated. It is well-formed after substitution, hence, no Substitution Failure occurs. We get: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:
We end up with the same specialization:
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:
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.
That above specialization exists only when it is well formed, so when
decltype( T::member )
is valid and not ambiguous. the specialization is so forhas_member<T , void>
as state in the comment.When you write
has_member<A>
, it ishas_member<A, void>
because of default template argument.And we have specialization for
has_member<A, void>
(so inherit fromtrue_type
) but we don't have specialization forhas_member<B, void>
(so we use the default definition : inherit fromfalse_type
)