Skip to content

Latest commit

 

History

History
142 lines (107 loc) · 7.71 KB

comparison_operator_rewrite.md

File metadata and controls

142 lines (107 loc) · 7.71 KB

Пользовательские операторы сравнения в C++20

С++, конечно, развивается медленным циклом в три года. Чтобы ничего не сломать. Большой успех -- большая ответственность... Но несмотря на всю медлительность и осторожность, новые стандарты C++ все равно умудряются подложить мину там, где никто не ожидает.

С++20 добавил долгожданный "операторо НЛО" <=>, three-way-comparison, позволяющим существенно сократить однообразный код для определения операций сравнения над пользовательскими типами.

struct Pair {
    int x;
    int y;

    auto operator<=>(const Pair&) const = default; 
    // все операции <, >, ==, !=, <=, >= -- выведены автоматически. Покомпонентное сравнение в порядке объявления полей!
};

// И вот мы уже можем сравнивать точки!
Pair {1, 2} < Pair { 2, 3 };

Ну почти.

<source>:8:10: error: 'strong_ordering' is not a member of 'std'
    8 |     auto operator <=> (const Pair&) const = default;
      |          ^~~~~~~~
<source>:1:1: note: 'std::strong_ordering' is defined in header '<compare>'; this is probably fixable by adding '#include <compare>'

Внезапно, вы обязаны подключить заголовок, если хотите использовать новую синтаксическую конструкцию c автоматической реализацией (через =default). Причем узнаете вы об этом только, когда попробуете использовать сравнение.

Добавим заголовок и все будет работать. Здорово? Конечно же! Но есть кое-что еще.

Не все типы можно или осмысленно упорядочивать. Иногда достаточно только равенства. В C++20 можно определить operator == и operator != будет выведен автоматически. Более того, реализация через = defalt также работает.

struct Pair {
    int x;
    int y;
    
    bool operator==(const Pair&) const = default; 
    
};

// operator != выведен автоматически
Pair {1, 2} != Pair{3, 4};

Другие операторы сравнения тоже можно автоматически определять по-умолчанию.

Прекрасно. Но сравнениями для одного и того же типа все не заканчивается. Иногда нам нужно уметь сравнивать объекты разных типов. Например std::string и std::string_view.

Как разработчики справлялись с этой задачей до C++20?

Ну, например, эксплуатируя неявное приведение типов, когда это уместно.

struct String;

struct StringView {
     // Разрешаем неявное приведение, ведь это удобно
     StringView(const String&) {}
     bool operator==(const StringView &) const { return true; }
};
   
struct String {
    bool operator==(const StringView &sv) const { 
        // а тут меняем порядок местами, ведь у StringView уже есть operator ==
        // overload resolution наедет его по первому аргументу
        // а ко второму (*this: String) можно применить неявное приведенине типа. Все отлично!
        return sv == *this; 
    }
};

String{} == String{};

Работает, компилируется c С++17, все отлично!

А без трюков 4 перегрузки нужно...

Ну хорошо. Переходим в C++20.

Program returned: 139
Program terminated with signal: SIGSEGV

Шикарно! Надеюсь, если вы компилировали без -Werror, у вас были хотя бы тесты.

<source>:14:19: warning: in C++20 this comparison calls the current function recursively with reversed arguments
   14 |         return sv == *this;
      |                ~~~^~~~~~~~
<source>: In function 'int main()':
<source>:19:31: warning: C++20 says that these are ambiguous, even though the second is reversed:
   19 |     return String{} == String{};

С++20 позаботился о разработчиках. И теперь им не нужно выдумывать странные перестановки аргументов местами чтоб писать поменьше перегрузок операторов сравнения. C++20 ввел правила переписывания всех операторов сравнения, так что компиляторы выполнят перестановку за вас. Даже если в ней нет надобности. И разумеется веселые и находчивые разработчики старых кодовых баз получат бесконечную рекурсию. А c ней и неопределенное поведение. И SIGSEGV от переполнения стека, если повезет.

Эти изменения в правила поиска перегрузок для операций сравнения имели и другие побочные эффекты. Их постарались исправить в предложении P2468R2 The Equality Operator You Are Looking For. Он принято и реализовано в C++23.

Но внезапную рекурсию все равно не убрали! Полагайтесь на предупреждения компилятора.

В С++20/23, если у вас есть неявное приведение между типами, не нужно больше ничего выдумывать -- определяйте операции для одного и того же типа, а комбинации будут получены автоматическими перестановками.

struct String {
    bool operator==(const String&) const = default; 
};
struct StringView {
     StringView(const String&) {}
     bool operator==(const StringView &) const = default;
};
   
String{} == StringView{String{}};
StringView{String{}} == String{};

Если неявного приведения нет, то достаточно определить только одну дополнительную перегрузку

struct String {
    bool operator==(const String&) const = default; 
};
struct StringView {
     explicit StringView(const String&) {}
     bool operator==(const StringView &) const = default;
};
   
bool operator == (const String& s, const StringView& sv) {
    return sv == StringView{s};
}

// Обе перестановки работают
String{} == StringView{String{}};
StringView{String{}} == String{};