Search code examples
c++c++17language-lawyersfinaevoid-t

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


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.


Solution

  • 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>);