본문으로 바로가기

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

 

autodefault 키워드가 가능합니다.

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  (0) 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