Use of void template argument in early detection idiom implementation
Asked Answered
F

1

7

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.

Filippo answered 15/11, 2016 at 13:6 Comment(9)
N4502's predecessor, N4436, has even more egregious typos (incorrect number of template arguments). So another set of typos would not be out of the question... ;-]Microbiology
Shouldn't it be using is_assignable = detect<T, assign_t, void> instead?Tactic
@Tactic I don't think the trailing void is necessary. I believe the root cause of the confusion is due to a typo, as suggested by Ildjarn. As the author mentions in the footnotes of the paper, perhaps this is a "thinko"Circumvent
@Rerito: Thanks for that. Can you come up with a scenario where the void is necessary?Filippo
@SteveLorimer I pointed out the said typo ;)Tactic
The trailing void in that case is unnecessary since the default template argument is specified in the non-specialized template.Tactic
@Rerito: Agreed. So I'm scratching my head as to why the authors chose that reason for reworking the idiom. I think perhaps a better argument could be made that they wanted to support detected_t that would give the type of Op<Args...>Filippo
@Filippo I thought the same, maybe an actual example would helpTactic
@Rerito: It looks like @Yakk's answer does require the void because of variadic template usage. Perhaps this scenario is what the authors had in mind rather than the simplistic single type Here's what I meanFilippo
F
1

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.

Filippo answered 15/11, 2016 at 16:42 Comment(1)
If this guy wants to get something approved like this, shouldn't s/he get it proof read and tested? Very annoying for the readers.Inquisitive

© 2022 - 2024 — McMap. All rights reserved.