ядро языкаcore language
Одна функция вместо шести — auto operator<=>(const Point&) const = default; синтезирует ==, !=, <, <=, >, >= из одного определения.One function instead of six — auto operator<=>(const Point&) const = default; synthesizes ==, !=, <, <=, >, >= from a single definition.
Оператор <=> ("spaceship") — оператор трёхстороннего сравнения: одна функция, из определения которой компилятор автоматически синтезирует все шесть операторов сравнения (==, !=, <, <=, >, >=), если они не написаны отдельно. Он не возвращает bool — его результат один из специальных типов category сравнения (std::strong_ordering, std::weak_ordering, std::partial_ordering), кодирующий не просто факт "меньше/больше", а категорию упорядочения самого типа.The <=> ("spaceship") operator is the three-way comparison operator: a single function from whose definition the compiler automatically synthesizes all six comparison operators (==, !=, <, <=, >, >=) when they aren't written separately. It doesn't return bool — its result is one of the special ordering-category types (std::strong_ordering, std::weak_ordering, std::partial_ordering), which encodes not just "less/greater" but the ordering category of the type itself.
struct Point {
int x, y;
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
bool operator!=(const Point& o) const { return !(*this == o); }
bool operator<(const Point& o) const { return x < o.x || (x == o.x && y < o.y); }
bool operator<=(const Point& o) const { return !(o < *this); }
bool operator>(const Point& o) const { return o < *this; }
bool operator>=(const Point& o) const { return !(*this < o); }
// добавили новое поле z — нужно синхронно поправить все шесть// added a new field z — all six now need updating in sync
};
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
// == , !=, <, <=, >, >= — все шесть уже доступны// == , !=, <, <=, >, >= — all six already available
};
До C++20 класс, желающий поддержать полное упорядочение, был обязан вручную писать все шесть операторов сравнения — рутинный и подверженный ошибкам boilerplate:Before C++20, a class that wanted to support full ordering had to hand-write all six comparison operators — routine, error-prone boilerplate:
Операторы < и <=, > и >= отличаются одной инверсией или одним знаком равенства — при копировании тела одного оператора в другой было легко перепутать строгое и нестрогое сравнение, получив логическую ошибку, которая не всегда всплывает в тестах.Operators < and <=, > and >= differ by one inversion or one equals sign — copying one operator's body into another made it easy to mix up strict and non-strict comparison, producing a logic bug that doesn't always surface in tests.
Каждое новое поле, влияющее на порядок сравнения, нужно было вручную дописать в тело каждого из шести операторов — забытое обновление одного из них давало класс, где, например, a == b истинно, а a < b при этом тоже истинно для тех же объектов, что нарушает базовые математические ожидания от отношений сравнения.Every new field affecting comparison order had to be manually added to the body of each of the six operators — forgetting to update one of them produced a class where, say, a == b was true while a < b was also true for the same objects, violating basic mathematical expectations of comparison relations.
До C++20 не было единого языкового механизма, из которого можно было бы вывести остальные операторы — каждый писался независимо, даже если логически они все выражаются через один и тот же базовый факт "меньше/равно/больше". <=> впервые даёт этому базовому факту прямое синтаксическое представление.Before C++20 there was no single language mechanism from which the remaining operators could be derived — each was written independently, even though they all logically express the same underlying fact of "less/equal/greater." <=> gives that underlying fact a direct syntactic representation for the first time.
a <=> b возвращает не bool, а объект одного из трёх типов категории сравнения. Компилятор транслирует выражения вроде a < b в (a <=> b) < 0 — эти типы перегружают сравнение с литералом 0, так что остальные пять операторов синтезируются автоматически из единственного определения <=>, если сам класс их не определяет отдельно. При = default компилятор генерирует лексикографическое сравнение по членам в порядке их объявления — ровно как если бы каждый член сравнивался по очереди, пока не найдётся первое отличие.An expression a <=> b returns not a bool but an object of one of three ordering-category types. The compiler translates expressions like a < b into (a <=> b) < 0 — these types overload comparison against the literal 0, so the remaining five operators are synthesized automatically from a single <=> definition, unless the class defines them separately. With = default, the compiler generates a member-wise lexicographic comparison in declaration order — exactly as if each member were compared in turn until the first difference is found.
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
// = default для <=> на агрегате из int-ов также генерирует ==// = default for <=> on an int-only aggregate also generates ==
};
Point a{1, 2}, b{1, 3};
bool less = a < b; // компилятор: (a <=> b) < 0 → сравнивает x, затем y// compiler: (a <=> b) < 0 → compares x, then y
// case, где нужно явное определение — регистронезависимая строка:// a case needing an explicit definition — a case-insensitive string:
struct CiString {
std::string s;
std::weak_ordering operator<=>(const CiString& o) const {
return to_lower(s) <=> to_lower(o.s);
// weak_ordering: "Abc" и "abc" равны при сравнении,// weak_ordering: "Abc" and "abc" compare equal,
// но не идентичны побайтово — не strong_ordering// but aren't byte-identical — not strong_ordering
}
bool operator==(const CiString& o) const {
return to_lower(s) == to_lower(o.s); // пишется отдельно, не выводится из <=>// written separately, not derived from <=>
}
};
Историческая причина, по которой == не выводится из <=> автоматически по умолчанию (если только оба не сгенерированы через = default одновременно) — производительность: равенство часто можно проверить быстрее, чем полное упорядочение (например, сравнение строк на равенство может завершиться на первом же различии длины, тогда как для получения порядка нужно досравнить содержимое). Заставлять == всегда идти через полный <=> означало бы платить за упорядочение там, где нужен был только факт равенства.The historical reason == isn't derived from <=> automatically by default (unless both are generated via = default together) is performance: equality can often be checked faster than full ordering (for instance, comparing strings for equality can short-circuit on a length mismatch alone, while producing an order requires comparing the actual contents). Forcing == to always go through a full <=> would mean paying for ordering where only an equality fact was needed.
Иногда сравнение объекта не является лексикографическим по хранимым полям вообще — например, версия вида major.minor.patch хранится как три числа, но "правильное" сравнение версий семантически не отличается от лексикографического по этим же трём полям в этом конкретном случае. Однако если версия хранится, скажем, вместе с necessary pre-release суффиксом ("1.2.0-rc1" < "1.2.0" по семантике semver, хотя строка "1.2.0-rc1" лексикографически больше строки "1.2.0"), сравнение по полям структуры данных перестаёт совпадать с сравнением по значению — и <=> приходится писать вручную, инкапсулируя это несовпадение:Sometimes an object's comparison isn't lexicographic over its stored fields at all — for instance, a major.minor.patch version is stored as three numbers, and in that particular case "correct" version comparison happens to coincide with lexicographic comparison over those same three fields. But if the version is stored together with, say, a pre-release suffix ("1.2.0-rc1" < "1.2.0" under semver semantics, even though the string "1.2.0-rc1" is lexicographically greater than the string "1.2.0"), comparing the data structure's fields stops matching comparison by value — and <=> has to be hand-written to encapsulate that mismatch:
struct SemVer {
int major, minor, patch;
std::string prerelease; // "" для релиза, "rc1"/"beta2" для пре-релиза// "" for a release, "rc1"/"beta2" for a pre-release
std::strong_ordering operator<=>(const SemVer& o) const {
if (auto c = major <=> o.major; c != 0) return c;
if (auto c = minor <=> o.minor; c != 0) return c;
if (auto c = patch <=> o.patch; c != 0) return c;
// семантика semver: пустой prerelease ("релиз") всегда СТАРШЕ непустого —// semver semantics: an empty prerelease ("a release") always outranks a non-empty one —
// это НЕ то же самое, что лексикографическое сравнение строк prerelease// this is NOT the same as a lexicographic comparison of the prerelease strings
if (prerelease.empty() && o.prerelease.empty()) return std::strong_ordering::equal;
if (prerelease.empty()) return std::strong_ordering::greater;
if (o.prerelease.empty()) return std::strong_ordering::less;
return prerelease <=> o.prerelease;
}
bool operator==(const SemVer& o) const = default;
};
// = default здесь дал бы "1.2.0" < "1.2.0-rc1" — неверно для semver// = default here would give "1.2.0" < "1.2.0-rc1" — wrong under semver
Типичный value type вроде денежной суммы или точки — сравнение по значению всех полей в порядке объявления совпадает с ожидаемой семантикой, поэтому = default подходит без оговорок.A typical value type like a money amount or a point — comparing by value across all fields in declaration order matches the expected semantics, so = default fits without caveats.
struct Money {
std::int64_t cents;
std::string currency;
auto operator<=>(const Money&) const = default;
// ==, !=, <, <=, >, >= — все шесть готовы// ==, !=, <, <=, >, >= — all six ready
};
Money a{100, "USD"}, b{250, "USD"};
bool cheaper = a < b; // true// true
Объекты, равные по сравнению, но не идентичные побайтово ("ABC" и "abc") — категория weak_ordering сигнализирует об этом прямо в типе возврата.Objects that compare equal but aren't byte-identical ("ABC" and "abc") — the weak_ordering category signals this directly in the return type.
struct CiKey {
std::string s;
std::weak_ordering operator<=>(const CiKey& o) const {
return to_lower(s) <=> to_lower(o.s);
}
bool operator==(const CiKey& o) const {
return to_lower(s) == to_lower(o.s);
}
};
Раз структура уже сравнима благодаря <=>, ranges::sort использует её сравнение автоматически — не нужна лямбда-компаратор, которая раньше писалась бы под каждую структуру заново.Since the struct is already comparable thanks to <=>, ranges::sort uses that comparison automatically — no lambda comparator, which used to be hand-written anew for every struct.
struct Order {
int priority;
double price;
auto operator<=>(const Order&) const = default;
};
std::vector<Order> orders = { {2, 9.5}, {1, 3.0}, {1, 1.5} };
std::ranges::sort(orders); // использует Order::operator<=> напрямую// uses Order::operator<=> directly
Поле-кэш или метаданные (например, счётчик обращений) не должны влиять на равенство объектов — = default тут не подходит в принципе, потому что учитывает все поля без исключения.A cache field or metadata (say, an access counter) shouldn't affect object equality — = default fundamentally doesn't fit here, since it accounts for every field with no exceptions.
struct CacheEntry {
std::string key;
std::string value;
mutable std::uint64_t hit_count = 0; // не участвует в сравнении// not part of comparison
std::strong_ordering operator<=>(const CacheEntry& o) const {
if (auto c = key <=> o.key; c != 0) return c;
return value <=> o.value; // hit_count сознательно пропущен// hit_count is deliberately skipped
}
bool operator==(const CacheEntry& o) const {
return key == o.key && value == o.value;
}
};
Если нужно именно равенство, а не полное упорядочение, — либо пишется отдельный operator==, либо оба (<=> и ==) генерируются через = default одновременно. Это две разные функции с разными путями оптимизации у компилятора — не стоит полагать, что определение одной автоматически даёт вторую бесплатно, если оба не default одновременно.If what's needed is specifically equality, not full ordering, either a separate operator== is written, or both (<=> and ==) are generated via = default together. These are two distinct functions with different compiler optimization paths — don't assume defining one automatically gives you the other for free unless both are defaulted together.
std::strong_ordering означает, что эквивалентные по сравнению объекты неотличимы (побайтово взаимозаменяемы); std::weak_ordering — что эквивалентные объекты могут отличаться, но ведут себя одинаково при дальнейшем сравнении (регистронезависимая строка — типичный пример); std::partial_ordering — что сравнение может быть "неупорядочено" (как NaN у float). Неверный выбор категории вводит в заблуждение код, полагающийся на эту гарантию — например, код, кэширующий объект как "тот же самый" на основании strong_ordering-равенства там, где на самом деле применим только weak_ordering.std::strong_ordering means objects that compare equal are indistinguishable (byte-interchangeable); std::weak_ordering means equivalent objects may differ but behave identically under further comparison (a case-insensitive string is the textbook example); std::partial_ordering means comparison can be "unordered" (like NaN for float). Picking the wrong category misleads code that relies on that guarantee — for instance, code that caches an object as "the same one" based on strong_ordering-equality where only weak_ordering actually applies.
Лексикографическое сравнение, сгенерированное = default, идёт строго по порядку объявления членов структуры — если нужен другой приоритет полей при сравнении (например, сравнивать сначала по id, а не по первому объявленному полю), = default не подойдёт, и <=> придётся написать вручную.The lexicographic comparison generated by = default strictly follows the struct's member declaration order — if a different field priority is needed for comparison (say, comparing by id first rather than by whichever field happens to be declared first), = default won't do, and <=> has to be written by hand.