ARTICLE AD BOX
Coming from C++17 and trying to integrate operator<=>. I've stumbled on a conundrum. There seem to be no way to implement a generic container-like class where its T is a mutually recursive std::variant. Everything is fine with a common set of pre-C++20 comparison operators, probably because they are completely defined at any point in time. Event if T is forward declared at the point of template declaration.
I've compiled a simplified set of test cases that make this issue evident. There Array and Map are not templated, but are simple structs, for simplicity, based on std::vector and std::map. This still clearly shows that comparison category type must be known upfront. Or operator<=> must be dropped altogether, with drawbacks (live on godbolt.com).
#include <compare> #include <vector> #include <map> #include <variant> #include <type_traits> /// Set 1-8, to choose between cases #define CASE 1 #define WANT_DEFAULT_WHEN_MEMEBER 0 /// Must be forward declared, as, e.g., using `std::vector<Var>` in Var's declaration would not be possible struct Array; struct Map; using Var = std::variant<int, Array, Map>; /// [CASE 1] Deduced comparison category type + default /// [COMPILES] No compiler #if CASE == 1 struct Array { bool operator==(const Array & that) const = default; auto operator<=>(const Array & that) const = default; std::vector<Var> value; }; struct Map { bool operator==(const Map & that) const = default; auto operator<=>(const Map & that) const = default; std::map<Var, Var> value; }; /// [CASE 2] Deduced comparison category type, declared outside, still default /// [COMPILES] No compiler #elif CASE == 2 struct Array { bool operator==(const Array & that) const = default; auto operator<=>(const Array & that) const; std::vector<Var> value; }; struct Map { bool operator==(const Map & that) const = default; auto operator<=>(const Map & that) const; std::map<Var, Var> value; }; auto Array::operator<=>(const Array & that) const = default; auto Map::operator<=>(const Map & that) const = default; /// [CASE 3] Declared outside, with concrete comparison category type /// [COMPILES] Only in GCC 15.2 #elif CASE == 3 struct Array { bool operator==(const Array & that) const = default; std::weak_ordering operator<=>(const Array & that) const; std::vector<Var> value; }; struct Map { bool operator==(const Map & that) const = default; std::weak_ordering operator<=>(const Map & that) const; std::map<Var, Var> value; }; std::weak_ordering Array::operator<=>(const Array & that) const { return value <=> that.value; } std::weak_ordering Map::operator<=>(const Map & that) const { return value <=> that.value; } /// [CASE 4] Template hack to delay instantiation, deduced comparison category type /// [COMPILES] Only MSVC 19 #elif CASE == 4 struct Array { bool operator==(const Array & that) const; template <typename T, typename = std::enable_if_t<std::is_same_v<T, Array>>> auto operator<=>(const T & that) const; std::vector<Var> value; }; struct Map { bool operator==(const Map & that) const; template <typename T, typename = std::enable_if_t<std::is_same_v<T, Map>>> auto operator<=>(const T & that) const; std::map<Var, Var> value; }; template <typename T, typename> auto Array::operator<=>(const T & that) const { return value <=> that.value; } template <typename T, typename> auto Map::operator<=>(const T & that) const { return value <=> that.value; } bool Array::operator==(const Array & that) const { return value == that.value; } bool Map::operator==(const Map & that) const { return value == that.value; } /// [CASE 5] Template hack to delay instantiation, concrete comparison category type /// [COMPILES] All compilers /// [LIMITATION] Requires knowing comparison category type, won't work if deducation is needed (template class) #elif CASE == 5 struct Array { bool operator==(const Array & that) const; template <typename T, typename = std::enable_if_t<std::is_same_v<T, Array>>> std::weak_ordering operator<=>(const T & that) const; std::vector<Var> value; }; struct Map { bool operator==(const Map & that) const; template <typename T, typename = std::enable_if_t<std::is_same_v<T, Map>>> std::weak_ordering operator<=>(const T & that) const; std::map<Var, Var> value; }; template <typename T, typename> std::weak_ordering Array::operator<=>(const T & that) const { return value <=> that.value; } template <typename T, typename> std::weak_ordering Map::operator<=>(const T & that) const { return value <=> that.value; } bool Array::operator==(const Array & that) const { return value == that.value; } bool Map::operator==(const Map & that) const { return value == that.value; } /// [CASE 6] As friends, no template hacks, declared before the type is defined /// [COMPILES] Only MSVC 19 /// [LIMITATION] Only as out of class friend (not critical) #elif CASE == 6 struct Array { friend bool operator==(const Array & _1, const Array & _2); friend auto operator<=>(const Array & _1, const Array & _2); std::vector<Var> value; }; struct Map { friend bool operator==(const Map & _1, const Map & _2); friend auto operator<=>(const Map & _1, const Map & _2); std::map<Var, Var> value; }; bool operator==(const Array & _1, const Array & _2) { return _1.value == _2.value; } auto operator<=>(const Array & _1, const Array & _2) { return _1.value <=> _2.value; } bool operator==(const Map & _1, const Map & _2) { return _1.value == _2.value; } auto operator<=>(const Map & _1, const Map & _2) { return _1.value <=> _2.value; } /// [CASE 7] As friends, no template hacks, declared before the type is defined, concrete comparison category type /// [COMPILES] Only MSVC 19 /// [LIMITATION] Only as out of class friend (not critical) /// [LIMITATION] Requires knowing comparison category type, won't work if deducation is needed (template class) #elif CASE == 7 struct Array { friend bool operator==(const Array & _1, const Array & _2); friend std::weak_ordering operator<=>(const Array & _1, const Array & _2); std::vector<Var> value; }; struct Map { friend bool operator==(const Map & _1, const Map & _2); friend std::weak_ordering operator<=>(const Map & _1, const Map & _2); std::map<Var, Var> value; }; bool operator==(const Array & _1, const Array & _2) { return _1.value == _2.value; } std::weak_ordering operator<=>(const Array & _1, const Array & _2) { return _1.value <=> _2.value; } bool operator==(const Map & _1, const Map & _2) { return _1.value == _2.value; } std::weak_ordering operator<=>(const Map & _1, const Map & _2) { return _1.value <=> _2.value; } /// [CASE 8] The full set of operators (pre C++20 way) /// [COMPILES] All compilers /// [LIMITATION] No `<=>` therefore, when contained by another class, that class can't default its `<=>`, /// at least in clang, but others seem incorrect as there is no way to deduce comparison category type /// I.e. won't work with `WANT_DEFAULT_WHEN_MEMEBER == 1` #elif CASE == 8 struct Array { /// Can't be defaulted: some type would be forward declared at that point. bool operator==(const Array & that) const; bool operator<(const Array & that) const; /// All other operators would follow here std::vector<Var> value; }; struct Map { /// Can't be defaulted: some type would be forward declared at that point. bool operator==(const Map & that) const; bool operator<(const Map & that) const; /// All other operators would follow here std::map<Var, Var> value; }; bool Array::operator==(const Array & that) const { return value == that.value; } bool Array::operator<(const Array & that) const { return value < that.value; } bool Map::operator==(const Map & that) const { return value == that.value; } bool Map::operator<(const Map & that) const { return value < that.value; } #endif /// Additional condition: defaulted `<=>` with a `Var` member #if WANT_DEFAULT_WHEN_MEMEBER struct Foo { auto operator<=>(const Foo &) const = default; Var v; }; #endif int main() { return Var{} == Var{} && Var{} < Var{}; }As of yet I couldn't find a solution. Probably I'm missing something, hence this question was posted. None of the 'working' solutions are perfect. [CASE 8] is closest to perfection and is basically pre-C++20, but lacks operator<=> which blocks default declarations of this operator in any class it is a member of. Strangely enough, only in clang, which may be a separate issue (enabled with #define WANT_DEFAULT_WHEN_MEMEBER 1). However, it feels like clang is correct here: how on earth can comparison category be deduced from == and < operators?
It looks like defect to me. Or maybe I'm missing something and it is possible to define Array and Map without explicitly knowing comparison category type (std::weak_ordering in test cases)?
