C++20 Spaceship Operator — 3방향 비교 연산자
C++20 이전에는 <, <=, >, >=, ==, != 6개의 비교 연산자를 각각 정의해야 했습니다. <=> (three-way comparison, 우주선 연산자)를 정의하면 나머지 연산자를 컴파일러가 자동으로 생성합니다.
1. 기본 문법
섹션 제목: “1. 기본 문법”#include <compare>
struct Point { int x, y;
// <=> 하나로 ==, !=, <, <=, >, >= 전부 생성 auto operator<=>(const Point&) const = default;};
int main() { Point a{1, 2}, b{1, 3}; bool lt = (a < b); // true bool eq = (a == b); // false}= default를 사용하면 멤버를 선언 순서대로 사전식 비교합니다.
2. 비교 카테고리
섹션 제목: “2. 비교 카테고리”| 타입 | 의미 | 예시 |
|---|---|---|
std::strong_ordering | 동치는 완전히 같음 | 정수, 포인터 |
std::weak_ordering | 동치이지만 구별 가능 | 대소문자 무시 문자열 |
std::partial_ordering | 비교 불가 값 존재 | 부동소수점(NaN) |
#include <compare>#include <cmath>
struct Vec2 { double x, y;
std::partial_ordering operator<=>(const Vec2& o) const { double mag1 = std::sqrt(x*x + y*y); double mag2 = std::sqrt(o.x*o.x + o.y*o.y); return mag1 <=> mag2; // NaN 포함 시 unordered 반환 }};3. 커스텀 구현
섹션 제목: “3. 커스텀 구현”멤버 중 일부만 비교에 포함하거나 순서를 바꾸고 싶을 때 직접 정의합니다.
#include <compare>#include <string>
struct Employee { int id; std::string name; double salary;
// id만으로 비교 std::strong_ordering operator<=>(const Employee& o) const { return id <=> o.id; }
bool operator==(const Employee& o) const { return id == o.id; }};4. std::compare_three_way 활용
섹션 제목: “4. std::compare_three_way 활용”#include <compare>#include <algorithm>#include <vector>
std::vector<int> v = {5, 3, 1, 4, 2};
// <=> 기반 정렬 (오름차순)std::ranges::sort(v, [](int a, int b) { return (a <=> b) < 0;});5. 기존 코드 마이그레이션
섹션 제목: “5. 기존 코드 마이그레이션”// Before (C++17)struct Rect { int w, h; bool operator<(const Rect& o) const { return w < o.w || (w == o.w && h < o.h); } bool operator==(const Rect& o) const { return w == o.w && h == o.h; } // ... 4개 더 필요};
// After (C++20)struct Rect { int w, h; auto operator<=>(const Rect&) const = default; // 6개 자동 생성};6. 주의사항
섹션 제목: “6. 주의사항”- 부동소수점 멤버가 있으면
= default는partial_ordering을 반환합니다. == default는<=> default와 별도로 생성됩니다 — 성능 최적화를 위해 두 개를 함께default선언하는 것이 권장됩니다.- 상속 계층에서는 기반 클래스의
<=>반환 타입과 파생 클래스의 반환 타입이 일치해야 합니다.
7. 혼합 타입 비교 (Heterogeneous Comparison)
섹션 제목: “7. 혼합 타입 비교 (Heterogeneous Comparison)”#include <compare>#include <string>
struct MyString{ std::string data;
// std::string과 직접 비교 가능하도록 설정 auto operator<=>(const MyString& other) const { return data <=> other.data; }
// 이종 비교 — const char* 와 비교 auto operator<=>(const char* other) const { return data <=> std::string(other); }
bool operator==(const MyString& other) const { return data == other.data; } bool operator==(const char* other) const { return data == other; }};
MyString s{"hello"};bool result = (s < MyString{"world"}); // truebool result2 = (s == "hello"); // true — 이종 비교8. 상속 계층에서의 사용
섹션 제목: “8. 상속 계층에서의 사용”#include <compare>
struct Shape{ int id; // 기반 클래스에서 default 비교 auto operator<=>(const Shape&) const = default;};
struct ColoredShape : Shape{ int color;
// 파생 클래스는 반환 타입을 기반 클래스와 맞추거나 더 좁게 설정 std::strong_ordering operator<=>(const ColoredShape& o) const { if (auto cmp = Shape::operator<=>(o); cmp != 0) return cmp; return color <=> o.color; }
bool operator==(const ColoredShape& o) const = default;};9. 컨테이너 정렬 활용
섹션 제목: “9. 컨테이너 정렬 활용”#include <compare>#include <vector>#include <algorithm>#include <map>
struct Version{ int major, minor, patch;
auto operator<=>(const Version&) const = default;
// 출력 편의 friend std::ostream& operator<<(std::ostream& os, const Version& v) { return os << v.major << '.' << v.minor << '.' << v.patch; }};
int main(){ std::vector<Version> versions = { {2, 0, 0}, {1, 9, 3}, {1, 0, 0}, {2, 1, 0}, {1, 9, 10} };
// <=> 덕분에 operator< 자동 생성 — sort 바로 사용 가능 std::sort(versions.begin(), versions.end()); // {1.0.0, 1.9.3, 1.9.10, 2.0.0, 2.1.0}
// map 키로 사용 (operator< 필요) std::map<Version, std::string> changelog; changelog[{1, 0, 0}] = "Initial release"; changelog[{2, 0, 0}] = "Major rewrite";}10. std::weak_ordering 활용 예
섹션 제목: “10. std::weak_ordering 활용 예”#include <compare>#include <cctype>#include <string>
// 대소문자 무시 문자열 비교 — weak_ordering (동치지만 구별 가능)struct CaseInsensitiveString{ std::string value;
std::weak_ordering operator<=>(const CaseInsensitiveString& o) const { // 각 문자를 소문자로 변환하여 비교 auto lhs = value; auto rhs = o.value; std::transform(lhs.begin(), lhs.end(), lhs.begin(), ::tolower); std::transform(rhs.begin(), rhs.end(), rhs.begin(), ::tolower);
if (lhs < rhs) return std::weak_ordering::less; if (lhs > rhs) return std::weak_ordering::greater; return std::weak_ordering::equivalent; // "Hello" == "hello" (동치, 동일은 아님) }
bool operator==(const CaseInsensitiveString& o) const { return (*this <=> o) == std::weak_ordering::equivalent; }};
CaseInsensitiveString a{"Hello"}, b{"hello"};bool eq = (a == b); // true — 동치// 하지만 a.value != b.value — 실제 문자열은 다름11. 비교 카테고리 결정 규칙
섹션 제목: “11. 비교 카테고리 결정 규칙”// 멤버의 타입에 따라 = default의 반환 타입이 결정됨struct A { int x; // strong_ordering double y; // partial_ordering (NaN 때문에)
// auto operator<=>(const A&) const = default; // → 반환 타입은 std::partial_ordering (가장 약한 카테고리 채택)};
// 부동소수점을 포함하면서 strong_ordering이 필요한 경우 직접 구현struct B { int id; double value; // 비교에서 제외하려면 직접 구현
std::strong_ordering operator<=>(const B& o) const { return id <=> o.id; // id만 비교 — strong_ordering 반환 } bool operator==(const B& o) const { return id == o.id; }};<=> 연산자 하나로 비교 관련 보일러플레이트를 대폭 줄일 수 있습니다.
| 비교 카테고리 | 의미 | 전형적 사용 |
|---|---|---|
std::strong_ordering | a == b이면 a와 b는 완전히 동일 | 정수, 포인터, 일반 값 타입 |
std::weak_ordering | a == b(동치)이지만 구별 가능 | 대소문자 무시, 정규화된 비교 |
std::partial_ordering | 일부 값은 비교 불가(unordered) | 부동소수점(NaN), 집합 포함 관계 |
실용 원칙:
- 단순 값 타입 →
= default(컴파일러가 적절한 카테고리 선택) - 일부 멤버만 비교에 포함 → 직접 구현
- 부동소수점 멤버 존재 시 →
= default는partial_ordering반환 주의