0

EDIT: this other question of mine focuses on a reduced version of this problem, possibly easier to understand.

I wrote a small snippet that reproduces the behaviour of std::experimental::is_detected (here). My implementation is basically taken from cppreference but I got rid of the Default template parameter.

My question is: in the following snippet, why does has_type (the condition to be checked) have to be a using declaration and cannot be, e.g. a struct (in which case is_detected returns a wrong result)?

/***** is_detected definition *****/
template<typename...Args>
using void_t = void;

template<typename Void, template<class...> class Op, typename ...Args>
struct Detector {
   static constexpr bool value = false;
};

template<template<class ...> class Op, typename ...Args>
struct Detector<void_t<Op<Args...>>, Op, Args...> {
   static constexpr bool value = true;
};

template<template<class...> class Op, typename...Args>
using is_detected_t = Detector<void, Op, Args...>;
/****************************/

/***** is_detected test *****/
// two dummy types on which to test a condition
struct Yes { using type = void; };
struct No { };

// the condition to test
template<typename T>
using has_type = typename T::type;
// struct has_type { using type = typename T::type; }; // does not work as intended!

int main() {
   static_assert(is_detected_t<has_type, Yes>::value, "");
   static_assert(!is_detected_t<has_type, No>::value, "");
   return 0;
}
blue
  • 2,683
  • 19
  • 29

1 Answers1

1

It might help to look at how has_type is actually used by the detector:

template<template<class ...> class Op, typename ...Args>
struct Detector<void_t<  Op<Args...>>, Op, Args...> {
//                       ^^  ^^
//                 has_type  Yes/No
   static constexpr bool value = true;
};

For this specialization to match the compiler must make sure that Op<Args...>, when replacing the parameters (Op and Args...) with the actual arguments (has_type and Yes/No), must name a type (since that's what the template void_t requires as first template argument).

Since has_type is not a type, but rather an alias of some type, it must look whether whatever is aliased names a type.

For Yes this will be Yes::type, which again is an alias of void. void is a type, so everything is fine, the specialization matches, value is true.

For No this will be No::type, which does not exist (No has no member type after all). Thus, the substitution fails (but this is not an error, SFINAE), the specialization cannot be used. Thus the compiler chooses the base template, where value is false.


Now what happens when you define has_type as follows:

template<typename T>
struct has_type { using type = typename T::type; }

Then above specialization needs (in the No case) that a type has_type<No> exists. has_type is a class template, which given some type (No is a type, so everything good) "produces" a type. Thus, has_type<No> is a type. Thus the specialization matches, value is true.

The members of has_type<No> are not needed at this point. You could even use template<typename> struct has_type; (only a declaration, no definition). In other words, it may be an incomplete type:

A template argument for a type template parameter must be a type-id, which may name an incomplete type [..]

http://en.cppreference.com/w/cpp/language/template_parameters

The contents only matter when the compiler actually needs them, e.g. for creating an object of that type:

// Class template with some random members.
template<typename T>
struct Foo {
    using baz = typename T::baz;
    constexpr static int value = T::value * 42;
};

// Class template which is even only declared
template<typename X> struct Bar; // no definition

// Does not use its template parameter in any way. Needs just a type name.
template<typename> struct Defer {};

int main() {
    Defer<Foo<int>> ok;
    Defer<Bar<int>> ok_too;
    // Foo<int> fail;
    // Bar<int> fail_too;
    return 0;
}

This mechanism is often used for "type tags", which e.g. can be used to create different types with identical "content" from a single template:

template<typename /* TAG */, typename ValueType>
struct value_of_strong_type {
  ValueType value;
  // ...
};

struct A_tag; // no definition
using A = value_of_strong_type<A_tag, int>;
struct B_tag; // no definition
using B = value_of_strong_type<B_tag, int>;

Both A and B behave identically, but are not convertible to each other, because they're completely different types.


To make the detector work with such class templates as you showed you need the following specialization:

template<template<class ...> class Op, typename ...Args>
struct Detector<void_t<typename Op<Args...>::type>, Op, Args...> {
//                     ^^^^^^^^            ^^^^^^
   static constexpr bool value = true;
};

Though you cannot just add it, otherwise you run into ambiguous resolution errors.

Daniel Jour
  • 15,896
  • 2
  • 36
  • 63