3
votes

I'd like to conditionally enable operator <=> overloads in my code depending on whether or not it is supported given the current version of the compiler and its command line options. For example, I'd like the following code to compile as C++14, 17, and 20 (this is essentially a sequel to this solution to a question I asked earlier):

#define SPACESHIP_OPERATOR_IS_SUPPORTED 1 // <--- i want this to be automatic

#if SPACESHIP_OPERATOR_IS_SUPPORTED
#include <compare>
#endif

template <int N> struct thing {
    // assume an implicit conversion to a "math-able" type exists:
    operator int () const { return 0; }
    // define a set of comparison operators for same N on rhs:
    bool operator == (const thing<N> &) const { return true; }
    bool operator != (const thing<N> &) const { return false; }
    bool operator < (const thing<N> &) const { return false; }
    bool operator > (const thing<N> &) const { return false; }
    bool operator <= (const thing<N> &) const { return true; }
    bool operator >= (const thing<N> &) const { return true; }
    int operator - (const thing<N> &) const { return 0; }
    // but explicitly delete ops for different N:
    // (see https://stackguides.com/questions/65468069)
    template <int R> bool operator == (const thing<R> &) const = delete; 
    template <int R> bool operator != (const thing<R> &) const = delete; 
    template <int R> bool operator < (const thing<R> &) const = delete; 
    template <int R> bool operator > (const thing<R> &) const = delete; 
    template <int R> bool operator <= (const thing<R> &) const = delete; 
    template <int R> bool operator >= (const thing<R> &) const = delete; 
    template <int R> int operator - (const thing<R> &) const = delete; 
    // but if i don't delete <=> for differing template parameters then things
    // like thing<0>() <=> thing<1>() will be allowed to compile because they'll
    // be implicitly converted to an int. so i *have* to delete it when supported.
#if SPACESHIP_OPERATOR_IS_SUPPORTED
    std::strong_ordering operator <=> (const thing<N> &) const = default;
    template <int R> std::strong_ordering operator <=> (const thing<R> &) const = delete;
#endif
};

int main () {
    thing<0> t0;
    thing<1> t1;
    (void)(t0 == t0);      // line 39
    //(void)(t0 == t1);    // line 40
#if SPACESHIP_OPERATOR_IS_SUPPORTED
    (void)(t0 <=> t0);     // line 42
    //(void)(t0 <=> t1);   // line 43
#endif
}

So, first a quick explanation of that:

  • The implicit operator int is a requirement.
  • Comparison operators are only defined for thing<int N>s with the same N.
  • The operators for mismatched Ns must be explicitly deleted, else the compiler will decide to implicitly apply operator int to both sides and use the int comparison instead (see linked question).
  • The intended behavior is for lines 40 and 43 (marked) to fail to compile.

Now, the reason I (think) I need to conditionally check for operator <=> support is:

  • The code needs to compile as C++14, 17, and 20.
  • If I don't overload <=> at all, then things like thing<0>() <=> thing<1>() are incorrectly allowed to compile (due to implicit conversion to int; same situation as the other operators). In other words: The default operator <=> is not appropriate in all cases, so I can't just let it be.
  • If I always write both <=> overloads, then the program fails to compile as C++14 and C++17, or presumably on compilers with incomplete C++20 implementations (although I have not encountered this).

The code above meets all of the requirements as long as I manually set SPACESHIP_OPERATOR_IS_SUPPORTED, but I want that to be automatic.

So, my question is: Is there a way to detect, at compile time, support for operator <=>, and conditionally enable code if it is present? Or is there some other way to make this work for C++14 thru 20?

I'm in a precompiler mindset but if there's some magic template solution, that works, too. I'd really like a compiler-independent solution, but at minimum I want this to work on GCC (5.x and higher) and MSVC (ideally 2015 and higher).

1

1 Answers

6
votes

This is what feature-test macros are for. There is a standing document that defines all the macros and their values; those are the macros and values that you check for, that all vendors agree to abide by.

Three-way-comparison specifically is a bit tricky because this is a feature that requires both language and library support. There is a language-level feature test macro, but it isn't intended for you (the user), it's intended for the standard library author to conditionally provide that functionality.

So what you really have to do this is this:

#if __has_include(<compare>)
#  include <compare>
#  if defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907
#    define SPACESHIP_OPERATOR_IS_SUPPORTED 1
#  endif
#endif

And now in the rest of your code you can check #ifdef SPACESHIP_OPERATOR_IS_SUPPORTED to conditionally provide <=>:

#ifdef SPACESHIP_OPERATOR_IS_SUPPORTED
    bool operator==(const thing<N> &) const = default;
    std::strong_ordering operator<=>(const thing<N> &) const = default;

    template <int R> bool operator==(const thing<R> &) const = delete; 
    template <int R> std::strong_ordering operator<=>(const thing<R> &) const = delete;
#else
    bool operator==(const thing<N> &) const { return true; }
    bool operator!=(const thing<N> &) const { return false; }
    bool operator< (const thing<N> &) const { return false; }
    bool operator> (const thing<N> &) const { return false; }
    bool operator<=(const thing<N> &) const { return true; }
    bool operator>=(const thing<N> &) const { return true; }

    template <int R> bool operator==(const thing<R> &) const = delete; 
    template <int R> bool operator!=(const thing<R> &) const = delete; 
    template <int R> bool operator< (const thing<R> &) const = delete; 
    template <int R> bool operator> (const thing<R> &) const = delete; 
    template <int R> bool operator<=(const thing<R> &) const = delete; 
    template <int R> bool operator>=(const thing<R> &) const = delete; 
#endif

You don't need to provide both defaulted <=> and all the relational operators. That's why we have <=>: so you can write <=> by itself. You still need to provide operator== but only because you're doing something special in needing to delete <=>.