반응형

c++20에서는 3중 비교 연산자(<=>)가 추가 되었습니다.

우주선과 비슷하게 생겨서 우주선 연산자라고 불리웁니다. 

3중 비교 연산자는 주어진 A와 B에 대해서 A < B인지, 또는 A == B 인지, A > B인지 판정합니다. 

3중 비교 연산자의 테스트 환경

3중 비교 연산자 컴파일러 지원 옵션

또한 프로젝트 우 클릭 -> 구성 속성 -> 일반 -> c++ 언어 표준을 ISO C++ 20 표준으로 수정하였습니다.

c++ 언어 표준 변경

VisualStudio 버전별 컴파일러 버전

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에 대해서 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

 

Default comparisons (since C++20) - cppreference.com

Provides a way to request the compiler to generate consistent comparison operators for a class. [edit] Syntax return-type class-name::operatorop( const class-name & ) const &(optional) = default; (1) friend return-type operatorop( const class-name &, const

en.cppreference.com

 

반응형

'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

+ Recent posts