C++20에는 '우주선' 연산자라는 애칭이 붙은 새로운 연산자가 추가되었습니다.
이 게시물은 우주선 연산자의 관계와, 이를 사용하여 정확하고 효율적인 양방향 비교를 작성하는 방법을 알아볼것입니다.
Relation strength
우주선 연산자는 'operator<=>'를 사용합니다. 공식적인 이름은 3방향 비교 연산자 (three-way comparison) 입니다.
두 객체를 비교한 다음 결과를 다음과 같이 비교하여 사용합니다.
(a <=> b) < 0 // b가 a보다 크다면 true, a < b
(a <=> b) > 0 // a가 b보다 크다면 true, a > b
(a <=> b) == 0 // a 와 b가 동등/동일 하다면 true, a == b
-1, 0, 1 로 비교합니다. strcmp와 비슷하다고 생각할수도 있습니다.
훨씬 더 복잡하지만, 훨씬 더 강력하게 설계되었습니다.
모든 평등 관계 ('0')가 평등하진 않습니다. C++의 우주선 연산자를 사용하면 객체 간의 순서와 평등을 표현할 수 있을 뿐만 아니라, 관계의 특성도 표현할 수 있습니다. 두 가지 예를 들면..
1. 직사각형
우리의 rectangle 클래스가 크기에 따라 비교 연산자를 정의할 수 있습니다.
1cm x 1cm 의 직사각형은 10cm x 10cm 의 직사각형 보다 작습니다.
근데, 1cm x 5cm 의 직사각형이 5cm x 1cm의 직사각형보다 작습니까? 이것들은 분명히 같지는 않지만 서로 작거나 크지도 않습니다.
모든 비교 연산자들 (<. <=, ==, >, >=)이 false를 반환할 것입니다.
하지만 일반적인 연산자들은 '(a == b || a < b || a > b) == true' 여야 합니다.
대신 이 경우 operator==는 완벽한 '동등'이 아닌 '동등'을 모형화 한다고 할 수 있습니다.
이를 'weak_ordering' 이라고 합니다.
2. 정사각형 정렬
위와 비슷하게, square 크기와 관련하여 비교 연산자를 정의하고자 합니다.
이 경우엔, 직사각형과는 다르게 동등하지만 같지 않다는 문제를 가지고 있지 않습니다.
즉, 두 개의 정사각형이 같은 면적을 가진다면 두 객체는 동등하다는 뜻입니다.
이 경우 operator==는 '동등'을 모형화 한다는 것이 아닌, 완벽한 '동등' 이라는 뜻입니다.
이를 'strong_ordering' 이라고 합니다.
관계의 설명
3방향 비교를 통해 관계의 강도(ordering)와 동등성 아니면 단지 순서 지정만 하는지 여부를 표현할 수 있습니다.
이는 operator<=>의 반환 유형을 통해 설명하는데, 5가지의 유형이 제공되며 더 강한 관계는 암묵적으로 약한 관계로 전환될 수 있습니다.
강한, 약한 순서는 위와 같습니다. 강하고 약하다는 것은 operator== 와 operator!= 에만 관련있습니다.
partial_ordering은 float 타입의 'NaN' 같은것도 허용한다는 점에서 weak_ordering보다 약합니다.
+0과 -0이 비트 수준에서 다른 것처럼, NaN == NaN은 동일하지 않습니다. partial_ordering 에서는 NaN <=> anything 을 Unordering으로 처리합니다. partial_ordring 에서는 -0 <=> +0 이 동등합니다. |
이런 종류는 여러 가지 용도가 있습니다.
1. 비교에 의해 어떠한 '관계'를 사용자에게 나타내므로, 행동이 보다 명확합니다.
2. strong_ordering일 경우에 효율적인 특수화(Specializations)를 잠재적으로 가능합니다.
예를 들어, strong_ordering으로 지정된 두 객체가 동일할 경우, 하나에 함수를 적용하면 다른 객체도 동일한 결과를 얻을 수 있습니다. 그로 인해 한 번만 수행하여 효율적으로 작업이 가능합니다.
3. 동일한 값이 항상 동일한 템플릿 인스턴스화 이름을 지정하도록, 타입이 강력한 동일성을 갖도록 요구하는 '클래스 유형 비-타입 템플릿 매개 변수' (class type non-type template parameters) 같은 언어 특징을 정의하는데 사용할 수 있습니다.
코드 작성
연산자 오버로딩을 통하여 3방향 연산자를 제공할 수 있습니다.
struct foo{
int i;
std::strong_ordering operator<=> (const foo& other){
return i<=> rhs.i;
}
};
auto나 default 키워드가 가능합니다.
struct MyInt{
int value;
explicit MyInt(int val) : value { val } {}
auto operator<=>(const MyInt& other) const {
return value <=> other.value;
}
};
struct MyDouble{
double value;
explicit constexpr MyDouble(double val) : value { val } {}
auto operator<=>(const MyDouble&) const = default;
};
MyInt와 MyDouble은 둘 다 잘 동작합니다.
그러나 ordering의 차이가 있습니다. MyInt의 경우, strong_ordering입니다.
MyDouble의 경우 partial_ordering입니다. 부동소숫점형의 경우 partial_ordering만 가능한데, NaN같은 타입은 ordering이 불가능하기 때문입니다. ( NaN == NaN은 false 입니다. )
컴파일러가 작성하는 우주선 연산자
컴파일러가 3방향 연산자를 작성하려면 <compare> 헤더가 필요합니다.
암시적으로 constexpr, noexcept를 추가합니다. 또한 사전순 비교(lexicographical comparison)입니다.
struct MyDouble{
double value;
explicit constexpr MyDouble(double val) : value { val } {}
auto operator<=>(const MyDouble&) const = default;
};
constexpr bool result{ MyDouble(2011) < MyDouble(2014) };
result는 컴파일 타임에 값을 얻어낼 수 있습니다.
Lexicographical Comparision
Lexicographical Comparision은 모든 기본 클래스가 선언 순서에 따라 왼쪽에서 오른쪽으로 모든 비정적 멤버와 비교됨을 의미합니다.
성능상의 이유로 컴파일러가 작성하는 operator==와 operator!=는 C++20에서 다르게 작동합니다.
struct Basics {
int i;
char c;
float f;
double d;
auto operator<=>(const Basics&) const = default;
};
struct Arrays {
int ai[1];
char ac[2];
float af[3];
double ad[2][2];
auto operator<=>(const Arrays&) const = default;
};
struct Bases : Basics, Arrays {
auto operator<=>(const Bases&) const = default;
};
int main() {
constexpr Bases a = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
constexpr Bases b = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
static_assert(a == b);
static_assert(!(a != b));
static_assert(!(a < b));
static_assert(a <= b);
static_assert(!(a > b));
static_assert(a >= b);
}
표현식 재작성 ( 연산자 합성, C++20 한정 )
컴파일러가 'a < b' 같은 것을 발견하면, 우주선 연산자를 사용하여 이를 '(a <=> b) < 0' 로 재작성합니다.
이 규칙은 6가지 비교 연산자 모두에 적용됩니다.
'a @ b' 가 '(a <=> b) @ 0' 로 변경됩니다. 만약 type(a) 에서 type(b)로의 변환이 없다면, 컴파일러는 새 표현식
'0 @ (b <=> a)' 를 생성합니다.
예를들어, '(a <=> b) < 0' 이 작동하지 않으면, 컴파일러는 '0 < (b <=> a)' 를 생성합니다. 비교 연산자의 대칭을 자동으로 처리합니다.
#include <compare>
#include <iostream>
class MyInt {
public:
constexpr MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const = default;
private:
int value;
};
int main() {
std::cout << std::endl;
constexpr MyInt myInt2011(2011);
constexpr MyInt myInt2014(2014);
constexpr int int2011(2011);
constexpr int int2014(2014);
if (myInt2011 < myInt2014) std::cout << "myInt2011 < myInt2014" << std::endl; // (1)
if ((myInt2011 <=> myInt2014) < 0) std::cout << "myInt2011 < myInt2014" << std::endl;
std::cout << std::endl;
if (myInt2011 < int2014) std:: cout << "myInt2011 < int2014" << std::endl; // (2)
if ((myInt2011 <=> int2014) < 0) std:: cout << "myInt2011 < int2014" << std::endl;
// std::strong_ordering::less
std::cout << std::endl;
if (int2011 < myInt2014) std::cout << "int2011 < myInt2014" << std::endl; // (3)
if (0 < (myInt2014 <=> int2011)) std:: cout << "int2011 < myInt2014" << std::endl; // (4)
// std::strong_ordering::greater
std::cout << std::endl;
}
1, 2, 3은 operator<와 operator<=>를 사용했습니다.
4 같은 경우 좀 흥미로운데, 재작성 하는 것을 보여줍니다.
어셈블리어로 보면 어떻게 연산자가 재작성 되는지 알 수 있습니다.
'C++ > Modern' 카테고리의 다른 글
C++20) std::format (2) | 2022.01.11 |
---|---|
C++20) Atomic Smart Pointers / std::atomic_flag (2) | 2021.07.23 |
C++20) Ranges - 2 (0) | 2021.07.19 |
C++20) Ranges - 1 (0) | 2021.06.15 |
C++20) Modules ( 모듈 ) - 2 (5) | 2021.03.14 |