Reliable way of ordering mulitple std::void_t partial specializations for type traits

92 Views Asked by At

I want to classify the "level of feature support" for a type. The type can contain aliases red and blue. It can contain just one, both , or neither, and I want to classify each of these cases with an enum class:

enum class holder_type {
    none,
    red,
    blue,
    both
};

This is a use-case for the "member detector idiom", and I've tried to implement it using std::void_t:

// primary template
template <class T, class = void, class = void>
struct holder_type_of
    : std::integral_constant<holder_type, holder_type::none> {};

// substitution failure if no alias T::red exists
template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::red>, Void>
    : std::integral_constant<holder_type, holder_type::red> {};

// substitution failure if no alias T::blue exists
template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::blue>, Void>
    : std::integral_constant<holder_type, holder_type::blue> {};

// substitution failure if one of the aliases doesn't exist
template <class T>
struct holder_type_of<T, std::void_t<typename T::blue>, std::void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::both> {};

However, the first and second partial specialization are redefinitions of each other. The following assertions fail to compile with clang, but succeed with GCC (as wanted).

static_assert(holder_type_of<red_holder>::value == holder_type::red);
static_assert(holder_type_of<blue_holder>::value == holder_type::blue);
<source>:36:8: error: redefinition of 'holder_type_of<T, std::void_t<typename T::blue>, Void>'
struct holder_type_of<T, std::void_t<typename T::blue>, Void>
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:32:8: note: previous definition is here
struct holder_type_of<T, std::void_t<typename T::red>, Void>
       ^

See live code on Compiler Explorer

How do I get my code to compile on all compilers, with all assertions passing? Also, is clang wrong here, and my code should actually work according to the standard?


Note: The example is intentionally artificial, but could could be used in practice for things like classifying iterators as random-access/forward/etc. I've run into this issue when trying to detect members of a trait type that the user can specialize themselves.

3

There are 3 best solutions below

3
Artyer On BEST ANSWER

This looks like CWG1980 (std::void_t<typename T::red> and std::void_t<typename T::blue> are deemed "equivalent" since they are both void, so they are redefinitions, but they are not functionally equivalent since they can be distinguished by substitution failure).

And even if you were to fix it by making it a dependent void, like:

template<typename...> struct dependent_void { using type = void; };
template<typename... T> using dependent_void_t = typename dependent_void<T...>::type;

It would make these two partial specializations:

template <class T, class Void>
struct holder_type_of<T, dependent_void_t<typename T::red>, Void>
    : std::integral_constant<holder_type, holder_type::red> {};
template <class T>
struct holder_type_of<T, dependent_void_t<typename T::blue>, dependent_void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::both> {};

ambiguous since the middle arguments dependent_void_t<typename T::red> and dependent_void_t<typename T::blue> are unrelated and not better one way or the other.

... So you can flip the red one's arguments so the last argument is the same and the dependent_void_t<typename T::blue> is better than just Void. You can even go back to std::void_t, as you aren't comparing multiple std::void_ts with different template arguments anymore:

template <class T, class Void>
struct holder_type_of<T, Void, std::void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::red> {};

https://godbolt.org/z/s6dPYWeao

This gets unmanageable pretty quickly for multiple conditions


"priority" based SFINAE detection is more easily done with function overloads though:

// Lowest priority overload
template<typename T>
constexpr holder_type holder_type_of_impl(...) { return holder_type::none; }
template<typename T, std::void_t<typename T::red>* = nullptr>
constexpr holder_type holder_type_of_impl(void*) { return holder_type::red; }
template<typename T, std::void_t<typename T::blue>* = nullptr>
constexpr holder_type holder_type_of_impl(long) { return holder_type::blue; }
template<typename T, std::void_t<typename T::red, typename T::blue>* = nullptr>
constexpr holder_type holder_type_of_impl(int) { return holder_type::both; }
// Highest priority overload

template <class T>
struct holder_type_of
    : std::integral_constant<holder_type, ::holder_type_of_impl<T>(0)> {};

// And if you have more than 4 priorities (probably a sign to break up your checks),
// you can use this handy template

template<unsigned N> struct priority : priority<N-1u> {};
template<> struct priority<0> {};

// Ordered from worst to best match for `f(priority<6>{})`
void f(priority<0>);
void f(priority<1>);
void f(priority<2>);
void f(priority<3>);
void f(priority<4>);
void f(priority<5>);
void f(priority<6>);
5
Sergey Kolesnik On

The problem with your code is caused by several specializations of <T, void_t<>>, which are evaluated to <T, void>, hence the redifinition error.
In order to circumvent this problem, you need some disambiguation for each specialization. Basically, I see no way of escaping writing the specializations for has_type.

However, life can be made easier by using tags.
Here's an example of a convenient API that reflects the usage in your artificial example:

#include <type_traits>

struct S {};

struct empty {};

struct red_holder {
    using red = S;
};

struct blue_holder {
    using blue = S;
};

struct both_holder {
    using red = S;
    using blue = S;
};

enum /*class*/ holder_type {
    none = 0,
    red = 1,
    blue = 2,
    both = 3
};

namespace tag 
{
    struct red_t {} constexpr red{};
    struct blue_t{} constexpr blue{};
}

template <typename Tag, typename T, typename = void>
struct _has : std::integral_constant<holder_type, holder_type::none>{};

template <typename T>
struct _has<tag::red_t, T, std::void_t<typename T::red>> 
    : std::integral_constant<holder_type, holder_type::red> {};

template <typename T>
struct _has<tag::blue_t, T, std::void_t<typename T::blue>> 
    : std::integral_constant<holder_type, holder_type::blue> {};

template <typename T, typename ... Tags>
constexpr holder_type has(Tags... tags) noexcept {
    const auto flags = (_has<Tags, T>{} | ...);
    return static_cast<holder_type>(flags);
}

template <typename T>
constexpr holder_type has() noexcept {
    return holder_type::none;
}


static_assert(has<empty>() == holder_type::none);
static_assert(has<red_holder>(tag::red) == holder_type::red);
static_assert(has<blue_holder>(tag::blue) == holder_type::blue);

static_assert(has<both_holder>(tag::red) == holder_type::red);
static_assert(has<both_holder>(tag::blue) == holder_type::blue);
static_assert(has<both_holder>(tag::blue, tag::red) == holder_type::both);

https://godbolt.org/z/qPax11GYf

Since (it seems to me) you need to have type->value mappings, I suggest using flags.


Using tags reduces the necessary boilerplate. Basically for each member detection you have to write 2 class templates (basic "false" + specialization).
By using tags, you now have to write only a specialization. But until we have a reflection support, you have to write it.

0
Jan Schultke On

As mentioned in other answers, the problem is that in:

template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::red>, Void>
    : std::integral_constant<holder_type, holder_type::red> {};

// substitution failure if no alias T::blue exists
template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::blue>, Void>

These two partial specializations are the same type. It's possible to solve this by swapping Void and std::void_t around in one of them, but this doesn't scale well to larger quantities of partial specializations, because we may need one extra template parameter for each.

A technique which is commonly used in libstdc++ is to add an int Bullet template parameter which is used for ordering the partial specializations:

// primary template, start at <Bullet = 0> and inherit from <Bullet + 1>
template <class T, int Bullet = 0, class Void = void>
struct holder_type_of : holder_type_of<T, Bullet + 1, Void> {};

// first attempt for <Bullet = 0>
// fall back onto <Bullet = 1> if substitution fails
template <class T>
struct holder_type_of<T, 0, std::void_t<typename T::blue, typename T::red>>
    : std::integral_constant<holder_type, holder_type::both> {};

// Bullet = 1, fall back to Bullet = 2
template <class T>
struct holder_type_of<T, 1, std::void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::red> {};

// Bullet = 2, fall back to Bullet = 3
template <class T>
struct holder_type_of<T, 2, std::void_t<typename T::blue>>
    : std::integral_constant<holder_type, holder_type::blue> {};

// Bullet = 3, substitution always succeeds, so we have a fallback case
template <class T>
struct holder_type_of<T, 3, void>
    : std::integral_constant<holder_type, holder_type::none> {};

See live example at Compiler Explorer