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.
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_t
s 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>);