기본 비교 연산

기본 비교 연산 (Default comparisons)

C++은 비교 연산자 함수들(<=>, ==, !=, <, >, <=, >=)을 = default로 명시적으로 정의해 놓으면, 컴파일러가 해당 클래스에 대한 기본 비교(default comparison)를 대신 생성해 줘요. 멤버별로 일일이 비교 코드를 쓰지 않아도 되도록 하는 기능이에요.

출처: cppreference

본문

정의 (Definition)

기본 비교 연산자 함수(defaulted comparison operator function) 란, 비템플릿 비교 연산자 함수(즉 <=>, ==, !=, <, >, <=, >=)로서 다음 조건을 모두 만족하는 함수를 말해요.

  • 어떤 클래스 C의 비정적 멤버이거나 friend예요.
  • C 안에서, 또는 C가 완전한(complete) 문맥에서 = default로 정의돼요.
  • 두 매개변수의 타입이 const C&이거나 C예요. 이때 (있다면) 암시적 객체 매개변수(implicit object parameter)는 첫 번째 매개변수로 간주해요.

이런 비교 연산자 함수를 클래스 C기본 비교 연산자 함수라고 불러요.

struct X
{
    bool operator==(const X&) const = default; // OK
    bool operator==(const X&) = default;       // 오류: 암시적 객체
                                               //        매개변수 타입이 X&
    bool operator==(this X, X) = default;      // OK
};

struct Y
{
    friend bool operator==(Y, Y) = default;        // OK
    friend bool operator==(Y, const Y&) = default; // 오류: 매개변수 타입이 다름
};

bool operator==(const Y&, const Y&) = default;     // 오류: Y의 friend가 아님

비교 연산자 함수의 암시적 정의에서의 이름 검색과 접근 검사는 그 함수 본문과 동등한 문맥에서 수행돼요. 그리고 클래스 안에 나타난 = default 정의는 그 함수의 첫 번째 선언이어야 해요.

기본 비교 순서 (Default comparison order)

클래스 C가 주어졌을 때, 부분 객체 목록(subobject list) 이 다음 순서로 만들어져요.

  • C의 직접 기반 클래스 부분 객체. 선언 순서대로.
  • C의 비정적 데이터 멤버. 선언 순서대로.
  • 어떤 멤버 부분 객체가 배열 타입이면, 첨자가 증가하는 순서대로 그 원소들의 나열로 확장돼요. 이 확장은 재귀적이에요. 즉 배열 타입의 배열 원소도, 배열 타입의 부분 객체가 없을 때까지 다시 확장돼요.

타입 C의 임의의 객체 x에 대해, 다음 설명에서:

  • nx에 대한 (확장된) 부분 객체 목록의 부분 객체 수라고 해요.
  • x_ix에 대한 (확장된) 부분 객체 목록의 i번째 부분 객체라고 해요. x_ix에 적용된 파생-기반 변환(derived-to-base conversion), 클래스 멤버 접근 표현식, 배열 첨자 표현식의 연속으로 형성돼요.
struct S {};

struct T : S
{
    int arr[2][2];
} t;

// "t"의 부분 객체 목록은 다음과 같은 5개의 부분 객체를 순서대로 갖습니다:
// (S)t → t[0][0] → t[0][1] → t[1][0] → t[1][1]

삼방 비교 (Three-way comparison)

클래스 타입에 대한 operator<=>어떤 반환 타입으로든 = default로 정의할 수 있어요.

비교 범주 타입 (Comparison category types)

비교 범주 타입(category type)은 세 가지가 있어요.

  • std::strong_ordering
  • std::weak_ordering
  • std::partial_ordering
타입 동등한 값들은.. 비교 불가한 값들은..
std::strong_ordering 구분 불가 허용 안 됨
std::weak_ordering 구분 가능 허용 안 됨
std::partial_ordering 구분 가능 허용

합성 삼방 비교 (Synthesized three-way comparison)

같은 타입의 glvalue ab 사이의, 타입 T에 대한 합성 삼방 비교(synthesized three-way comparison) 는 다음과 같이 정의돼요.

  • a <=> b에 대한 오버로드 해석이 사용 가능한 후보를 만들고, 그걸 static_cast로 명시적으로 T로 변환할 수 있으면, 합성 비교는 static_cast<T>(a <=> b)예요.
  • 그렇지 않고 다음 조건 중 하나라도 만족하면, 합성 비교는 정의되지 않아요.
    • a <=> b에 대한 오버로드 해석이 viable 후보를 적어도 하나 찾음.
    • T가 비교 범주 타입이 아님.
    • a == b에 대한 오버로드 해석이 사용 가능한 후보를 만들지 못함.
    • a < b에 대한 오버로드 해석이 사용 가능한 후보를 만들지 못함.
  • 그렇지 않고 Tstd::strong_ordering이면, 합성 비교는 다음과 같아요.
a == b ? std::strong_ordering::equal :
a < b  ? std::strong_ordering::less :
         std::strong_ordering::greater
  • 그렇지 않고 Tstd::weak_ordering이면, 합성 비교는 다음과 같아요.
a == b ? std::weak_ordering::equivalent :
a < b  ? std::weak_ordering::less :
         std::weak_ordering::greater
  • 그 외(Tstd::partial_ordering)의 경우, 합성 비교는 다음과 같아요.
a == b ? std::partial_ordering::equivalent :
a < b  ? std::partial_ordering::less :
b < a  ? std::partial_ordering::greater :
         std::partial_ordering::unordered

자리표시자 반환 타입 (Placeholder return type)

클래스 타입 C에 대한 기본 비교 연산자 함수(operator<=>)의 선언된 반환 타입이 auto라면, 반환 타입은 타입 C의 객체 x대응하는 부분 객체들 사이의 삼방 비교의 반환 타입들로부터 추론돼요.

x의 (확장된) 부분 객체 목록의 각 부분 객체 x_i에 대해:

  • x_i <=> x_i에 대한 오버로드 해석을 수행해요. 그 해석이 사용 가능한 후보를 만들지 못하면, 기본 operator<=>삭제된(deleted) 것으로 정의돼요.
  • x_i <=> x_i의 타입의 cv-미한정(unqualified) 버전을 R_i라고 할 때, R_i가 비교 범주 타입이 아니면 기본 operator<=>는 deleted로 정의돼요.
  • 기본 operator<=>가 deleted로 정의되지 않았다면, 그 반환 타입은 std::common_comparison_category_t<R_1, R_2, ..., R_n>으로 추론돼요.

비자리표시자 반환 타입 (Non-placeholder return type)

기본 operator<=>의 선언된 반환 타입이 auto가 아니라면, 그 타입은 decltype(auto) 같은 자리표시자 타입을 담을 수 없어요.

만약 x의 (확장된) 부분 객체 목록에, 선언된 반환 타입으로 x_ix_i 사이의 합성 삼방 비교가 정의되지 않는 부분 객체 x_i가 있다면, 기본 operator<=>는 deleted로 정의돼요.

비교 결과 (Comparison result)

xy를 기본 operator<=>의 매개변수라고 할 때, xy의 (확장된) 부분 객체 목록의 각 부분 객체를 각각 x_i, y_i라고 표기해요. xy 사이의 기본 삼방 비교는 대응하는 부분 객체 x_iy_ii가 증가하는 순서로 비교해 가며 수행돼요.

R을 (추론됐을 수 있는) 반환 타입이라고 할 때, x_iy_i 사이의 비교 결과는 x_iy_i 사이의, 타입 R에 대한 합성 삼방 비교의 결과예요.

xy 사이의 기본 삼방 비교 중, x_iy_i 사이의 부분 객체별 비교가 v_i라는 결과를 만들고, v_i != 0bool로 문맥 변환했을 때 true가 되면, 반환값은 v_i의 복사본이 돼요 (나머지 부분 객체는 비교하지 않아요).

그렇지 않으면 반환값은 static_cast<R>(std::strong_ordering::equal)이에요.

#include <compare>
#include <iostream>
#include <set>

struct Point
{
    int x;
    int y;
    auto operator<=>(const Point&) const = default;
    /* 비교가 아닌 함수들 */
};

int main()
{
    Point pt1{1, 1}, pt2{1, 2};
    std::set<Point> s; // OK
    s.insert(pt1);     // OK

    // 이항 비교 연산자 함수를 명시적으로 정의할 필요는 없습니다:
    // operator==는 암시적으로 선언됩니다 (아래 참고)
    // 다른 후보들의 오버로드 해석은 재작성된(rewritten) 후보를 선택합니다
    std::cout << std::boolalpha
        << (pt1 == pt2) << ' '  // false
        << (pt1 != pt2) << ' '  // true
        << (pt1 <  pt2) << ' '  // true
        << (pt1 <= pt2) << ' '  // true
        << (pt1 >  pt2) << ' '  // false
        << (pt1 >= pt2) << ' '; // false
}

동등 비교 (Equality comparison)

명시적 선언 (Explicit declaration)

클래스 타입에 대한 operator==반환 타입 bool= default 정의할 수 있어요.

클래스 C와 타입 C의 객체 x가 주어졌을 때, x의 (확장된) 부분 객체 목록에, x_i == x_i에 대한 오버로드 해석이 사용 가능한 후보를 만들지 못하는 부분 객체 x_i가 있다면, 기본 operator==는 deleted로 정의돼요.

xy를 기본 operator==의 매개변수라고 할 때, xy의 (확장된) 부분 객체 목록의 각 부분 객체를 각각 x_i, y_i라고 표기해요. xy 사이의 기본 동등 비교는 대응하는 부분 객체 x_iy_i를 i가 증가하는 순서로 비교해 가며 수행돼요.

x_iy_i 사이의 비교 결과는 x_i == y_i의 결과예요.

xy 사이의 기본 동등 비교 중, x_iy_i 사이의 부분 객체별 비교가 v_i라는 결과를 만들고, v_ibool로 문맥 변환했을 때 false가 되면, 반환값은 false예요 (나머지 부분 객체는 비교하지 않아요).

그렇지 않으면 반환값은 true예요.

#include <iostream>

struct Point
{
    int x;
    int y;
    bool operator==(const Point&) const = default;
    /* 비교가 아닌 함수들 */
};

int main()
{
    Point pt1{3, 5}, pt2{2, 5};
    std::cout << std::boolalpha
        << (pt1 != pt2) << '\n'  // true
        << (pt1 == pt1) << '\n'; // true

    struct [[maybe_unused]] { int x{}, y{}; } p, q;
    // if (p == q) {} // 오류: operator==가 정의되지 않음
}

암시적 선언 (Implicit declaration)

클래스 Coperator==라는 이름의 멤버나 friend를 명시적으로 선언하지 않았다면, = default로 정의된 각 operator<=>에 대해 함수 하나가 암시적으로 선언돼요. 각 암시적으로 선언된 operator==는 해당 기본 operator<=>와 같은 접근성과 함수 정의, 같은 클래스 스코프를 가지되, 다음이 바뀌어요.

  • 선언자 식별자가 operator==로 바뀌어요.
  • 반환 타입이 bool로 바뀌어요.
template<typename T>
struct X
{
    friend constexpr std::partial_ordering operator<=>(X, X)
        requires (sizeof(T) != 1) = default;
    // 암시적으로 선언: friend constexpr bool operator==(X, X)
    //                         requires (sizeof(T) != 1) = default;

    [[nodiscard]] virtual std::strong_ordering operator<=>(const X&) const = default;
    // 암시적으로 선언: [[nodiscard]] virtual bool
    //                     operator==(const X&) const = default;
};

2차 비교 (Secondary comparison)

클래스 타입에 대한 2차 비교 연산자 함수(!=, <, >, <=, >=)는 반환 타입 bool= default 정의할 수 있어요.

@를 다섯 개의 2차 비교 연산자 중 하나라고 할 때, 매개변수 x, y를 가진 각 기본 operator@에 대해, deleted로 정의할지 결정하기 위해 (기본 operator@를 후보로 고려하지 않고) 오버로드 해석을 최대 두 번 수행해요.

  • 첫 번째 오버로드 해석x @ y에 대해 수행돼요. 그 해석이 사용 가능한 후보를 만들지 못하거나, 선택된 후보가 재작성된(rewritten) 후보가 아니면, 기본 operator@는 deleted로 정의돼요. 이런 경우 두 번째 오버로드 해석은 없어요.
  • 두 번째 오버로드 해석x @ y의 선택된 재작성된 후보에 대해 수행돼요. 그 해석이 사용 가능한 후보를 만들지 못하면 기본 operator@는 deleted로 정의돼요.
  • x @ ybool로 암시적으로 변환할 수 없으면, 기본 operator@는 deleted로 정의돼요.

기본 operator@가 deleted로 정의되지 않았다면, operator@x @ y를 돌려줘요.

struct HasNoRelational {};

struct C
{
    friend HasNoRelational operator<=>(const C&, const C&);
    bool operator<(const C&) const = default; // OK, 함수는 defaulted
};

키워드 (Keywords)

이 기능과 관련된 키워드는 default예요.

결함 보고 (Defect reports)

다음의 동작을 바꾸는 결함 보고가 이전에 공표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 공표된 동작 올바른 동작
CWG 2539 C++20 명시적 변환이 없어도 합성 삼방 비교가 static_cast를 선택했음 이 경우 static_cast를 선택하지 않음
CWG 2546 C++20 x @ y의 오버로드 해석이 사용 불가능한 재작성된 후보를 선택해도 기본 2차 operator@가 deleted로 정의되지 않았음 이 경우 deleted로 정의
CWG 2547 C++20 클래스가 아닌 것에 대한 비교 연산자 함수를 default로 할 수 있는지 불명확했음 default로 할 수 없음
CWG 2568 C++20 비교 연산자 함수의 암시적 정의가 멤버 접근 규칙을 위반할 수 있었음 함수 본문과 동등한 문맥에서 접근 검사 수행

더 알아보기 (Learn more)

  • 오버로드된 연산자 호출의 오버로드 해석 — 비교 연산자를 호출할 때 어떤 후보가 선택되는지의 메커니즘. (cppreference)
  • 내장 삼방 비교 연산자 — 기본 제공 <=> 연산자의 동작. (cppreference)
  • 비교 연산자 오버로딩 — 비교 연산자를 직접 오버로딩하는 방법. (cppreference)