c++20에서는 3중 비교 연산자(<=>)가 추가 되었습니다.
우주선과 비슷하게 생겨서 우주선 연산자라고 불리웁니다.
3중 비교 연산자는 주어진 A와 B에 대해서 A < B인지, 또는 A == B 인지, A > B인지 판정합니다.
3중 비교 연산자의 테스트 환경
또한 프로젝트 우 클릭 -> 구성 속성 -> 일반 -> c++ 언어 표준을 ISO C++ 20 표준으로 수정하였습니다.
c++ 20 이전의 순서 판정
3중 비교 연산자의 편리함을 소개 하기 위해서 c++20 이전에 상등/대소 관계 연산자를 설명 드립니다.
c++20 이전에는 ==, !=, >, >=, <, <= 연산자 중에 하나라도 정의 했다면 나머지 관계들도 모두 정의 해야 합니다.
(어떠한 형식을 직관적으로 사용하려면 최소한 SemiRegular(준정규) 형식을 충족 해야 합니다.)
struct MyInt {
int value;
explicit MyInt(int val) : value{val} {}
bool operator == ( const MyInt & rhs) const {
return value == rhs.value;
}
bool operator != ( const MyInt & rhs) const {
return !(*this == rhs);
}
bool operator < ( const MyInt & rhs) const {
return value < rhs.value;
}
bool operator <= ( const MyInt & rhs) const {
return !(rhs < *this)
}
bool operator > ( const MyInt & rhs) const {
return rhs < *this;
}
bool operator >= ( const MyInt & rhs) const {
return !(*this < rhs);
}
};
c++ 20 부터의 순서 판정
c++20 부터는 3중 비교 연산자를 직접 정의 하거나 = default 를 사용해서 컴파일러에 의해 생성된 연산자를 사용할 수
있습니다.
3중 비교 연산자를 사용하면 코드 중복을 크게 줄일 수 있습니다.
#include <iostream>
#include <compare>
using namespace std;
struct MyInt {
int value;
explicit MyInt(int val) : value{ val } {}
auto operator==(const MyInt& rhs) const {
return value == rhs.value;
}
// 3중 비교 연산자에 대한 명시적 선언
auto operator<=>(const MyInt& rhs) const {
return value <=> rhs.value;
}
};
struct MyIntDefualt {
int value;
// 3중 비교 연산자에 대한 암시적 선언
explicit MyIntDefualt(int val) : value{ val } {}
auto operator<=>(const MyIntDefualt& rhs) const = default;
};
int main()
{
cout << boolalpha;
MyInt myInt1(2017);
MyInt myInt2(2020);
cout << (myInt1 < myInt2) << '\n'; // true
cout << (myInt1 > myInt2) << '\n'; // false
cout << (myInt1 == myInt2) << '\n'; // false
MyIntDefualt myIntD1(2017);
MyIntDefualt myIntD2(2020);
cout << (myIntD1 < myIntD2) << '\n'; // true
cout << (myIntD1 > myIntD2) << '\n'; // false
cout << (myIntD1 == myIntD2) << '\n'; // false
}
3중 비교 연산자 구현 방식
3중 비교 연산자의 구현 방식을 설명 드리기 전에 c++20의 비교 범주(comparison category) 설명을 진행 하겠습니다.
비교 범주(comparison category)
비교 범주는 3가지 성질의 성립 여부를 체크해야 합니다.
- 관계 연산자 성질
- T는 여섯가지 비교 연산자(==, !=, < ,<=,>,>=)를 모두 지원해야 합니다.
- 동치(equivalent) 성질
- T의 객체 A와 B가 동치일 때는 f(A) 와 f(B)의 결과도 동일 해야 합니다.
- f는 전달받은 인수를 읽기 전용으로 사용한다고 가정합니다.
- 즉 동치인 값은 구별 할 수 없습니다.
- T의 객체 A와 B가 동치일 때는 f(A) 와 f(B)의 결과도 동일 해야 합니다.
- 비교 가능 성질
- T의 모든 값은 비교 가능 해야 합니다.
- A와 B에 대해서 A < B, A == B, A > B 중에 하나가 참이어야 합니다.
- 예를 들어 부동소수점은 Nan을 가질 수 있는데 이 값은 비교 가능 성질을 충족하지 못합니다.
비교 범주 성질 | std::strong_ordering | std::weak_ordering | std::partial_ordering |
관계 연산자 성질 | 성립 | 성립 | 성립 |
비교 가능 성질 | 성립 | 성립 | |
동치(equivalent) 성질 | 성립 |
3중 비교 연산자 리턴 값
3중 비교 연산자는 문맥에 따라서 std::strong_ordering, std::weak_ordering, std::partial_ordering를 리턴합니다.
(예를 들어 객체가 가지고 있는 변수들이 비교 범주를 얼마나 충족하느냐에 따라서 달라짐)
3종류의 객체들의 구현내용을 확인해 보면 다음과 같은 단순한 구조로 되어 있습니다.
- 구조체이며 _Compare_t(signed char) 형식의 `_Value`라는 하나의 멤버 변수를 가집니다.
- 리터럴 0과 비교하는 operator들이 선언되어 있습니다.
- 자주 사용하는 불변 객체(less, greater, equal, equivalent, unordered)를 전역 상수로 등록하여 최적화 합니다.
- less : -1
- greater : 1
- equal : 0
- equivalent : 0
- unordered : -128
_Value에 대한 설정 조건은 첫번째 인자를 기준으로 두번째 인수와의 관계에 따라서 다름과 같이 설정 됩니다.
- 작다면 less(-1)
- 같으면 equal(0), equivalent (0)
- 크다면 greater(1)
- 알수 없다면 unordered(-128)
#include <iostream>
#include <compare>
using namespace std;
struct MyInt {
int value;
// 3중 비교 연산자에 대한 암시적 선언
explicit MyInt(int val) : value{ val } {}
auto operator<=>(const MyInt& rhs) const = default;
};
int main()
{
MyInt cpp17(2017);
MyInt cpp20(2020);
// result_threeway = strong_ordering::less(-1)
auto result_threeway_le = cpp17 <=> cpp20;
// result_threeway = strong_ordering::greater(1)
auto result_threeway_gr = cpp20 <=> cpp17;
// result_threeway = strong_ordering::equal(0)
auto result_threeway_eq = cpp20 <=> cpp20;
cout << boolalpha;
cout << "result_threeway_le : " << (result_threeway_le < 0) << '\n'; // true
cout << "result_threeway_gr : " << (result_threeway_gr > 0) << '\n'; // true
cout << "result_threeway_eq : " << (result_threeway_eq == 0) << '\n'; // true
}
표현식 재작성
c++ 20에서는 표현식 재작성이라는 개념이 적용 됩니다.
// 비교 연산자들이 다음의 형태로 변경 됩니다.
a OP b ==> (a <=> b) OP 0
기존 코드 | 변경 코드 |
a > b | (a <=> b) > 0 |
a >= b | (a <=> b) >= 0 |
a < b | (a <=> b) < 0 |
a <= b | (a <=> b) <= 0 |
위와 같은 구조로 되어 있기 때문에 3중 비교 연산자로 비교 연산자를 처리할 수 있습니다.
// 원본 코드
#include <iostream>
#include <compare>
using namespace std;
struct MyInt {
//생략..
};
int main()
{
MyInt cpp17(2017);
MyInt cpp20(2020);
auto result1 = cpp17 == cpp20;
auto result2 = cpp17 != cpp20;
auto result3 = cpp17 < cpp20;
auto result4 = cpp17 <= cpp20;
auto result5 = cpp17 > cpp20;
auto result6 = cpp17 >= cpp20;
}
cppInsights 라는 곳을 이용하면 컴파일러에 의해 재작성된 구문을 확인 할 수 있습니다.
// 컴파일러에 의해서 재작성된 구문
#include <iostream>
#include <compare>
using namespace std;
struct MyInt
{
// 생략..
inline constexpr std::strong_ordering operator<=>(const MyInt & rhs) const noexcept = default;
// ==는 따로 구현됩니다.
inline constexpr bool operator==(const MyInt & rhs) const noexcept = default;
};
int main()
{
MyInt cpp17 = MyInt(2017);
MyInt cpp20 = MyInt(2020);
// cpp17 == cpp20
bool result1 = cpp17.operator==(cpp20);
// !(cpp17 == cpp20)
bool result2 = !cpp17.operator==(cpp20);
// (cpp17 <=> cpp20) < 0
bool result3 = operator<(cpp17.operator<=>(cpp20), __cmp_cat::__unspec(0));
// (cpp17 <=> cpp20) <= 0
bool result4 = operator<=(cpp17.operator<=>(cpp20), __cmp_cat::__unspec(0));
// (cpp17 <=> cpp20) > 0
bool result5 = operator>(cpp17.operator<=>(cpp20), __cmp_cat::__unspec(0));
// (cpp17 <=> cpp20) >= 0
bool result6 = operator>=(cpp17.operator<=>(cpp20), __cmp_cat::__unspec(0));
return 0;
}
컴파일러 생성하는 3중 비교 연산자
위에서 설명드린 c++ Insights 라는 사이트를 이용하면 컴파일러에 의해 생성되는 3중 비교 연산자를 확인 할 수 있습니다.
operator== 의 정의도 추가되는 것을 확인 할 수 있습니다.
// c++Insights 에서 생성되는 암시적 3중 연산자
#include <compare>
struct MyInt
{
int value;
inline explicit MyInt(int val)
: value{val}
{
}
inline constexpr std::strong_ordering operator<=>(const MyInt & rhs) const /* noexcept */ = default;
inline constexpr bool operator==(const MyInt & rhs) const /* noexcept */ = default;
};
컴파일 시점 비교
컴파일러에 의해서 생성되는 3중 비교 연산자는 constexpr 이므로 문맥에 따라서 컴파일 시점에 사용 할 수 있습니다.
아래 예제에서는 MyInt D1, D2를 리터럴 값을 통해서 생성하기 때문에 컴파일 시점에 사용 할 수 있는 변수가 되었고
3중 비교 연산자도 컴파일 시점에 수행 될 수 있습니다.
struct MyInt {
int value;
// 생성자를 constexpr로 지정합니다.
explicit constexpr MyInt(int val) : value{ val } {}
auto operator<=>(const MyInt& rhs) const = default;
};
int main()
{
// 인자를 리터럴로 전달 받았으므로 D1, D2 컴파일 시점에 사용 가능
constexpr MyInt D1(2017);
constexpr MyInt D2(2020);
// operator<=> 수식도 constexpr 이므로 컴파일 시점 사용 가능
constexpr bool res = (D1 < D2);
cout << boolalpha;
cout << "D1 < D2 : " << res << '\n';
return 0;
}
어휘순 비교
컴파일러가 생성한 3중 비교 연산자는 어휘순 비교를 사용합니다.
어휘순 비교란 모든 기반 클래스의 모든 비정적(non-static) 멤버들을 선언 순서대로 비교 하는 것을 말합니다.
아래 예제서는 Lexico를 어휘순으로 비교 할 때는 a -> b -> c -> d -> e 순서대로 비교를 진행합니다.
struct Basics {
int a;
float b;
double c;
char d;
auto operator<=>(const Basics &) const = default;
};
struct Lexico : public Basics {
int e;
auto operator<=>(const Lexico&) const = default;
};
int main()
{
constexpr Lexico l1 = { 1, 2.0f, 3.0f, 4, 5 };
constexpr Lexico l2 = { 1, 2.0f, 3.0f, 4, 5 };
cout << boolalpha;
cout << "l1 == l2 : " << (l1 == l2) << '\n';
cout << "l1 != l2 : " << (l1 != l2) << '\n';
cout << "l1 < l2 : " << (l1 < l2) << '\n';
cout << "l1 <= l2 : " << (l1 <= l2) << '\n';
cout << "l1 > l2 : " << (l1 > l2) << '\n';
cout << "l1 >= l2 : " << (l1 >= l2) << '\n';
/*
[결 과]
l1 == l2 : true
l1 != l2 : false
l1 < l2 : false
l1 <= l2 : true
l1 > l2 : false
l1 >= l2 : true
*/
// 어휘 순으로 비교하다가 e에서 l4가 크다는 판정 확인
constexpr Lexico l3 = { 1, 2.0f, 3.0f, 4, 5 };
constexpr Lexico l4 = { 1, 2.0f, 3.0f, 4, 6 };
cout << boolalpha;
cout << "l3 == l4 : " << (l3 == l4) << '\n';
cout << "l3 != l4 : " << (l3 != l4) << '\n';
cout << "l3 < l4 : " << (l3 < l4) << '\n';
cout << "l3 <= l4 : " << (l3 <= l4) << '\n';
cout << "l3 > l4 : " << (l3 > l4) << '\n';
cout << "l3 >= l4 : " << (l3 >= l4) << '\n';
/*
[결 과]
l3 == l4 : false
l3 != l4 : true
l3 < l4 : true
l3 <= l4 : true
l3 > l4 : false
l3 >= l4 : false
*/
return 0;
}
최적화된 == 연산자와 != 연산자
비교 연산을 할 때 문자열이나 벡터 같은 형식들에 대해서는 최적화할 여지가 있습니다.
전체 비교전에 객체의 길이를 먼저 비교 한 후에 다르다면 false를 리턴하는 최적화를 진행 할 수 있습니다.
하지만 컴파일러가 생성하는 3중 비교 연산자는 어휘순 비교를 통해서 처음부터 끝까지 비교 합니다.
이러한 수정 사항을 P1185R2라는 수정안이 발표 했고 msvc에서는 2019 16.2 버전에 적용 되었음을 확인 할 수 있습니다.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1185r2.html
다음과 같이 수정합니다.
template<typename T>
bool operator==(vector<T> const& lhs, vector<T> const& rhs)
{
// short-circuit on size early
const size_t size = lhs.size();
if (size != rhs.size()) {
return false;
}
for (size_t i = 0; i != size; ++i) {
// use ==, not <=>, in all nested comparisons
if (lhs[i] != rhs[i]) {
return false;
}
}
return true;
}
참조
https://en.cppreference.com/w/cpp/language/operator_comparison#Three-way_comparison
https://en.cppreference.com/w/cpp/language/default_comparisons
'c++ > modern c++' 카테고리의 다른 글
[c++17] fold expression(폴드 표현식) (0) | 2022.06.22 |
---|---|
[c++20] consteval과 constinit (0) | 2022.06.16 |
[c++20] module(모듈) (0) | 2022.06.13 |
[c++20] Concept (0) | 2022.06.10 |
[c++] condition variable(조건 변수) (0) | 2020.07.10 |