Use of void template argument in early detection i

2019-01-26 10:04发布

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.

1条回答
Juvenile、少年°
2楼-- · 2019-01-26 10:15

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.

查看更多
登录 后发表回答