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>andstd::void_t<typename T::blue>are deemed "equivalent" since they are bothvoid, 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:It would make these two partial specializations:
ambiguous since the middle arguments
dependent_void_t<typename T::red>anddependent_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 justVoid. You can even go back tostd::void_t, as you aren't comparing multiplestd::void_ts with different template arguments anymore: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: