기본 비교 연산
기본 비교 연산 (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에 대해, 다음 설명에서:
n을x에 대한 (확장된) 부분 객체 목록의 부분 객체 수라고 해요.x_i를x에 대한 (확장된) 부분 객체 목록의 i번째 부분 객체라고 해요.x_i는x에 적용된 파생-기반 변환(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_orderingstd::weak_orderingstd::partial_ordering
| 타입 | 동등한 값들은.. | 비교 불가한 값들은.. |
|---|---|---|
std::strong_ordering |
구분 불가 | 허용 안 됨 |
std::weak_ordering |
구분 가능 | 허용 안 됨 |
std::partial_ordering |
구분 가능 | 허용 |
합성 삼방 비교 (Synthesized three-way comparison)
같은 타입의 glvalue a와 b 사이의, 타입 T에 대한 합성 삼방 비교(synthesized three-way comparison) 는 다음과 같이 정의돼요.
a <=> b에 대한 오버로드 해석이 사용 가능한 후보를 만들고, 그걸static_cast로 명시적으로T로 변환할 수 있으면, 합성 비교는static_cast<T>(a <=> b)예요.- 그렇지 않고 다음 조건 중 하나라도 만족하면, 합성 비교는 정의되지 않아요.
a <=> b에 대한 오버로드 해석이 viable 후보를 적어도 하나 찾음.T가 비교 범주 타입이 아님.a == b에 대한 오버로드 해석이 사용 가능한 후보를 만들지 못함.a < b에 대한 오버로드 해석이 사용 가능한 후보를 만들지 못함.
- 그렇지 않고
T가std::strong_ordering이면, 합성 비교는 다음과 같아요.
a == b ? std::strong_ordering::equal :
a < b ? std::strong_ordering::less :
std::strong_ordering::greater
- 그렇지 않고
T가std::weak_ordering이면, 합성 비교는 다음과 같아요.
a == b ? std::weak_ordering::equivalent :
a < b ? std::weak_ordering::less :
std::weak_ordering::greater
- 그 외(
T가std::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_i와 x_i 사이의 합성 삼방 비교가 정의되지 않는 부분 객체 x_i가 있다면, 기본 operator<=>는 deleted로 정의돼요.
비교 결과 (Comparison result)
x와 y를 기본 operator<=>의 매개변수라고 할 때, x와 y의 (확장된) 부분 객체 목록의 각 부분 객체를 각각 x_i, y_i라고 표기해요. x와 y 사이의 기본 삼방 비교는 대응하는 부분 객체 x_i와 y_i를 i가 증가하는 순서로 비교해 가며 수행돼요.
R을 (추론됐을 수 있는) 반환 타입이라고 할 때, x_i와 y_i 사이의 비교 결과는 x_i와 y_i 사이의, 타입 R에 대한 합성 삼방 비교의 결과예요.
x와 y 사이의 기본 삼방 비교 중, x_i와 y_i 사이의 부분 객체별 비교가 v_i라는 결과를 만들고, v_i != 0을 bool로 문맥 변환했을 때 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로 정의돼요.
x와 y를 기본 operator==의 매개변수라고 할 때, x와 y의 (확장된) 부분 객체 목록의 각 부분 객체를 각각 x_i, y_i라고 표기해요. x와 y 사이의 기본 동등 비교는 대응하는 부분 객체 x_i와 y_i를 i가 증가하는 순서로 비교해 가며 수행돼요.
x_i와 y_i 사이의 비교 결과는 x_i == y_i의 결과예요.
x와 y 사이의 기본 동등 비교 중, x_i와 y_i 사이의 부분 객체별 비교가 v_i라는 결과를 만들고, v_i를 bool로 문맥 변환했을 때 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)
클래스 C가 operator==라는 이름의 멤버나 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 @ y를bool로 암시적으로 변환할 수 없으면, 기본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)