Judging on how the authors wrote their final implementation of is_detected
, they intended that Op
be a variadic template, which allows one to express many more concepts:
(Also pulled from n4502)
// primary template handles all types not supporting the archetypal Op:
template< class Default
, class // always void; supplied externally
, template<class...> class Op
, class... Args
>
struct
detector
{
using value_t = false_type;
using type = Default;
};
// the specialization recognizes and handles only types supporting Op:
template< class Default
, template<class...> class Op
, class... Args
>
struct
detector<Default, void_t<Op<Args...>>, Op, Args...>
{
using value_t = true_type;
using type = Op<Args...>;
};
//...
template< template<class...> class Op, class... Args >
using
is_detected = typename detector<void, void, Op, Args...>::value_t;
When you get into a scenario like this, a void
becomes necessary so that template specialization will match the true_type
version when Op<Args...>
is a valid expression.
Here's my tweak on the original detect to be variadic:
// primary template handles all types not supporting the operation:
template< class T, template<class...> class Trait, class... TraitArgs >
struct
detect : std::false_type { };
// specialization recognizes/validates only types supporting the archetype:
template< class T, template<class...> class Trait, class... TraitArgs >
struct
detect< T, Trait, std::void_t<Trait<T, TraitArgs...>>, TraitArgs... > : std::true_type { };
template<class T, template<class...> class Trait, class... TraitArgs>
using is_detected_t = typename detect<T, Trait, void, TraitArgs...>::type;
template<class T, template<class...> class Trait, class... TraitArgs>
constexpr bool is_detected_v = detect<T, Trait, void, TraitArgs...>::value;
Note that I renamed Op
to Trait
, Args
to TraitArgs
, and used std::void_t
which made it into C++17.
Now let's define a trait to test for a function named Foo
that can may or may not accept certain parameter types:
template<class T, class... Args>
using HasFoo_t = decltype( std::declval<T>().Foo(std::declval<Args>()...));
Now we can get a type (true_type
or false_type
) given some T
and our trait:
template< class T, class... Args>
using has_foo_t = is_detected_t<T, HasFoo_t, Args...>;
And finally, we can also "just check" to see if the trait is valid for some provided T
and Args
:
template<class T, class... Args>
constexpr bool has_foo_v = is_detected_v<T, HasFoo_t, Args...>;
Here's a struct to start testing:
struct A
{
void Foo(int)
{
std::cout << "A::Foo(int)
";
}
};
And finally the test(s):
std::cout << std::boolalpha << has_foo_v<A, int> << std::endl; //true
std::cout << std::boolalpha << has_foo_v<A> << std::endl; // false
If I remove the void
from my is_detected_t
and is_detected_v
implementations, then the primary specialization is chosen, and I get false
(Example).
This is because the void
is there so as to match std::void_t<Trait<T, TraitArgs...>>
which if you recall will have a type of void
if the template argument is well-formed. If the template argument is not well-formed, then std::void_t<Trait<T, TraitArgs...>>
is not a good match and it will revert to the default specialization (false_type
).
When we remove void
from our call (and simply leave TraitArgs...
in its place) then we cannot match the std::void_t<Trait<T, TraitArgs...>>
argument in the true_type
specialization.
Also note that if std::void_t<Trait<T, TraitArgs...>>
is well-formed, it simply provides a void
type to the class... TraitArgs
argument in the primary template, so we don't need to define an extra template parameter to receive void
.
In conclusion, the authors wanted to remove the void
that would end up in client code, hence their slightly more complicated implementation later in the paper.
Thanks to @Rerito for pointing out this answer where Yakk also puts in a little extra work to avoid the pesky void
in client code.