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 thatOp
be a variadic template, which allows one to express many more concepts:(Also pulled from n4502)
When you get into a scenario like this, a
void
becomes necessary so that template specialization will match thetrue_type
version whenOp<Args...>
is a valid expression.Here's my tweak on the original detect to be variadic:
Note that I renamed
Op
toTrait
,Args
toTraitArgs
, and usedstd::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:Now we can get a type (
true_type
orfalse_type
) given someT
and our trait:And finally, we can also "just check" to see if the trait is valid for some provided
T
andArgs
:Here's a struct to start testing:
And finally the test(s):
If I remove the
void
from myis_detected_t
andis_detected_v
implementations, then the primary specialization is chosen, and I getfalse
(Example).This is because the
void
is there so as to matchstd::void_t<Trait<T, TraitArgs...>>
which if you recall will have a type ofvoid
if the template argument is well-formed. If the template argument is not well-formed, thenstd::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 leaveTraitArgs...
in its place) then we cannot match thestd::void_t<Trait<T, TraitArgs...>>
argument in thetrue_type
specialization.Also note that if
std::void_t<Trait<T, TraitArgs...>>
is well-formed, it simply provides avoid
type to theclass... TraitArgs
argument in the primary template, so we don't need to define an extra template parameter to receivevoid
.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.