In n4502 the authors describe an early implementation of the detect idiom that encapsulates the void_t
trick. Here's its definition along with usage for defining a trait for is_assignable
(really it's is_copy_assignable
)
template<class...>
using void_t = void;
// primary template handles all types not supporting the operation:
template< class, template<class> class, class = void_t< > >
struct
detect : std::false_type { };
// specialization recognizes/validates only types supporting the archetype:
template< class T, template<class> class Op >
struct
detect< T, Op, void_t<Op<T>> > : std::true_type { };
// archetypal expression for assignment operation:
template< class T >
using
assign_t = decltype( std::declval<T&>() = std::declval<T const &>() );
// trait corresponding to that archetype:
template< class T >
using
is_assignable = detect<void, assign_t, T>;
They mention that they don't like this because of the void
used in the is_assignable
trait:
Although the resulting code was significantly more comprehensible than the original, we disliked the above detect interface because the
void
argument in the metafunction call is an implementation detail that shouldn’t leak out to client code.
However, the void
doesn't make any sense to me in the first place. If I try to use this type trait to detect if int
is copy assignable, I get std::false_type
Demo.
If I rewrite is_assignable
as:
template< class T >
using
is_assignable = detect<T, assign_t>;
Which makes more sense to me, then the trait appears to work correctly: Demo
So my question here is Am I misunderstanding something in this document, or was it simply a typo?
If it was a typo, then I don't understand why the authors felt the need to discuss how they didn't like void
leaking out, which makes me pretty sure I'm just missing something.
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)\n";
}
};
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With