콘텐츠로 이동

C++20 Spaceship Operator — 3방향 비교 연산자

C++20 이전에는 <, <=, >, >=, ==, != 6개의 비교 연산자를 각각 정의해야 했습니다. <=> (three-way comparison, 우주선 연산자)를 정의하면 나머지 연산자를 컴파일러가 자동으로 생성합니다.


#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를 사용하면 멤버를 선언 순서대로 사전식 비교합니다.


타입의미예시
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 반환
}
};

멤버 중 일부만 비교에 포함하거나 순서를 바꾸고 싶을 때 직접 정의합니다.

#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;
}
};

#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;
});

// 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개 자동 생성
};

  • 부동소수점 멤버가 있으면 = defaultpartial_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"}); // true
bool result2 = (s == "hello"); // true — 이종 비교

#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;
};

#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";
}

#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 — 실제 문자열은 다름

// 멤버의 타입에 따라 = 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_orderinga == b이면 a와 b는 완전히 동일정수, 포인터, 일반 값 타입
std::weak_orderinga == b(동치)이지만 구별 가능대소문자 무시, 정규화된 비교
std::partial_ordering일부 값은 비교 불가(unordered)부동소수점(NaN), 집합 포함 관계

실용 원칙:

  • 단순 값 타입 → = default (컴파일러가 적절한 카테고리 선택)
  • 일부 멤버만 비교에 포함 → 직접 구현
  • 부동소수점 멤버 존재 시 → = defaultpartial_ordering 반환 주의