10 분 소요

(C++20~) 삼중 비교 연산자

기존에는 비교 연산자를 구현하기 위해서 ==, !=, <, >, <=, >= 6개의 비교 연산자를 각각 구현해야 했는데요(연산자 오버로딩 참고),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class T {
    int m_Val;
public:
    explicit T(int val) : m_Val{val} {}
    bool operator ==(const T& other) const {return m_Val == other.m_Val;} // < 로부터 구현하면, !(*this < other || other < *this)로 할 수 있습니다. 단 < 을 2회 하므로 비효율적입니다.
    bool operator !=(const T& other) const {return !(*this == other);} // == 로부터 구현
    bool operator <(const T& other) const {return m_Val < other.m_Val;}
    bool operator >(const T& other) const {return other < *this;} // < 로부터 구현
    bool operator <=(const T& other) const {return !(other < *this);} // < 로부터 구현
    bool operator >=(const T& other) const {return !(*this < other);} // < 로부터 구현
};  

EXPECT_TRUE(T{10} == T{10});  
EXPECT_TRUE(T{10} != T{20}); 
EXPECT_TRUE(T{10} < T{20}); 
EXPECT_TRUE(T{20} > T{10}); 
EXPECT_TRUE(T{10} <= T{20} && T{10} <= T{10}); 
EXPECT_TRUE(T{20} >= T{10} && T{10} >= T{10}); 

C++20 부터는 삼중 비교 연산자(<=>)가 추가되어 ==삼중 비교 연산자(<=>)만 정의하면 6개의 비교 연산자가 컴파일러에 의해 자동으로 정의됩니다.(strong_ordering비교 카테고리를 참고하세요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T_20 {
    int m_Val;
public:
    explicit T_20(int val) : m_Val{val} {}
    
    std::strong_ordering operator <=>(const T_20& other) const {return m_Val <=> other.m_Val;}
    bool operator ==(const T_20& other) const {return m_Val == other.m_Val;}
};  

EXPECT_TRUE(T_20{10} == T_20{10});  
EXPECT_TRUE(T_20{10} != T_20{20}); 
EXPECT_TRUE(T_20{10} < T_20{20}); 
EXPECT_TRUE(T_20{20} > T_20{10}); 
EXPECT_TRUE(T_20{10} <= T_20{20} && T_20{10} <= T_20{10}); 
EXPECT_TRUE(T_20{20} >= T_20{10} && T_20{10} >= T_20{10}); 

STL에서는 대부분 삼중 비교 연산자로 구현하고, ==, !=, <, >, <=, >= 6개의 비교 연산자를 deprecate 했습니다.(vector등 참고)

(C++20~) 삼중 비교 관련 유틸리티들이 추가되었습니다.

삼중 비교 연산자 비교

<=> 연산자를 정의하면, 기존 ==, !=, <, >, <=, >= 로 비교할 수 있을 뿐만 아니라, <=>로 직접 비교할 수 있습니다.

이때 strcmp()와 유사하게 다음과 같이 비교합니다.

  • 작음 : (left <=> right) < 0
  • 같음 : (left <=> right) == 0
  • 큼 : (left <=> right) > 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T_20 {
    int m_Val;
public:
    explicit T_20(int val) : m_Val{val} {}
    
    std::strong_ordering operator <=>(const T_20& other) const {return m_Val <=> other.m_Val;}
    bool operator ==(const T_20& other) const {return m_Val == other.m_Val;}
};  

EXPECT_TRUE((T_20{10} <=> T_20{10}) == 0);  // left == right
EXPECT_TRUE((T_20{10} <=> T_20{20}) != 0); // left != right
EXPECT_TRUE((T_20{10} <=> T_20{20}) < 0); // left < right
EXPECT_TRUE((T_20{20} <=> T_20{10}) > 0);  // left > right
EXPECT_TRUE((T_20{10} <=> T_20{20}) == 0 || (T_20{10} <=> T_20{10}) <= 0); // left <= right
EXPECT_TRUE((T_20{20} <=> T_20{10}) == 0 || (T_20{10} <=> T_20{10}) >= 0); // left >= right

또한 삼중 비교 연산자는 이종 타입과의 비교를 지원합니다.

이전에는 이종 타입과의 비교를 위해 케이스마다 함수 구현을 해야 했습니다. 다음 Tint와 비교하기 위해 무려 12개의 멤버 함수를 구현해야 하며, int < T와 같이 int가 왼쪽에 오는 경우도 지원하려면, 비멤버 버전 6개를 추가로 만들어야 했습니다.(연산자 오버로딩+ 연산자 비멤버 버전 참고) 도합 18개의 함수 구현이 필요하죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class T {
    int m_Val;
public:
    explicit T(int val) : m_Val{val} {} 

    // T op T 의 형태로 비교
    bool operator ==(const T& other) const {return m_Val == other.m_Val;} // < 로부터 구현하면, !(*this < other || other < *this)로 할 수 있습니다. 단 < 을 2회 하므로 비효율적입니다.
    bool operator !=(const T& other) const {return !(*this == other);} // == 로부터 구현
    bool operator <(const T& other) const {return m_Val < other.m_Val;}
    bool operator >(const T& other) const {return other < *this;} // < 로부터 구현
    bool operator <=(const T& other) const {return !(other < *this);} // < 로부터 구현
    bool operator >=(const T& other) const {return !(*this < other);} // < 로부터 구현

    // T op int 의 형태로 비교
    bool operator ==(int val) const {return m_Val == val;} 
    bool operator !=(int val) const {return !(m_Val == val);} 
    bool operator <(int val) const {return m_Val < val;}
    bool operator >(int val) const {return val < m_Val;} 
    bool operator <=(int val) const {return !(val < m_Val);} 
    bool operator >=(int val) const {return !(m_Val < val);} 
}; 
// int op T의 형태로 비교
bool operator ==(int left, const T& right) {return T{left} == right;}
bool operator !=(int left, const T& right) {return T{left} != right;}
bool operator <(int left, const T& right) {return T{left} < right;}
bool operator >(int left, const T& right) {return T{left} > right;}
bool operator <=(int left, const T& right) {return T{left} <= right;}
bool operator >=(int left, const T& right) {return T{left} >= right;}

EXPECT_TRUE(T{10} < T{20}); // T op T
EXPECT_TRUE(T{10} < 20); // T op int  
EXPECT_TRUE(10 < T{20}); // int op T

삼중 비교 연산자(left <=> right) < 0 이 유효하지 않다면 비교 연산자의 대칭성(a < bb > a 입니다.)을 고려하여 (right <=> left) > 0 로 변환하여 비교합니다. 즉, 왼쪽 인수와 오른쪽 인수를 바꿔서도 비교하므로, 비멤버 버전을 만들 필요가 없습니다.

따라서, int인자로 받는 삼중 비교 연산자== 멤버 버전만 추가하면 T_20int를 비교할 수 있습니다.

  • (T_20{10} <=> 10) < 0와 같이 호출하면, operator <=>(int val) 버전을 호출합니다.

  • (10 <=> T_20{20}) < 0와 같이 호출하면, 정수 10에는 T_20과 비교하는 <=>가 정의되지 않았으므로, (T_20{20} <=> 10) > 0으로 변환하여 operator <=>(int val) 버전을 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class T_20 {
    int m_Val;
public:
    explicit T_20(int val) : m_Val{val} {}
    
    // T_20 op T_20
    std::strong_ordering operator <=>(const T_20& other) const {return m_Val <=> other.m_Val;}
    bool operator ==(const T_20& other) const {return m_Val == other.m_Val;}

    // T_20 op int 또는 int op T_20
    std::strong_ordering operator <=>(int val) const {return m_Val <=> val;}
    bool operator ==(int val) const {return m_Val == val;}
};  

EXPECT_TRUE((T_20{10} <=> T_20{20}) < 0); // T_20 op T_20
EXPECT_TRUE((T_20{10} <=> 20) < 0); // T_20 op int  
EXPECT_TRUE((10 <=> T_20{20}) < 0); // int op T_20. (T_20 <=> 10) > 0으로 변경합니다.  

EXPECT_TRUE(T_20{10} < T_20{20}); // T_20 op T_20
EXPECT_TRUE(T_20{10} < 20); // T_20 op int  
EXPECT_TRUE(10 < T_20{20}); // int op T_20. T_20 > 10으로 변경합니다.  

만약 T_20explicit를 사용하지 않아 암시적으로 형변환 된다면, operator <=>(const T_20& other) 버전만 구현해도 됩니다. 하지만 암시적 형변환은 나쁜 것이니 피해야 하겠죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class T_20 {
    int m_Val;
public:
    T_20(int val) : m_Val{val} {} // (△) 비권장. explicit 가 없습니다. 암시적 형변환 됩니다.
    
    // T_20 op T_20
    std::strong_ordering operator <=>(const T_20& other) const {return m_Val <=> other.m_Val;}
    bool operator ==(const T_20& other) const {return m_Val == other.m_Val;}
};  

EXPECT_TRUE((T_20{10} <=> T_20{20}) < 0); // T_20 op T_20
EXPECT_TRUE((T_20{10} <=> 20) < 0); // int를 암시적으로 T_20으로 형변환  
EXPECT_TRUE((10 <=> T_20{20}) < 0); // int op T_20. (T_20 <=> 10) > 0으로 변경후 int를 암시적으로 T_20으로 형변환

EXPECT_TRUE(T_20{10} < T_20{20}); // T_20 op T_20
EXPECT_TRUE(T_20{10} < 20); // int를 암시적으로 형변환.
EXPECT_TRUE(10 < T_20{20}); // int op T_20. T_20 > 10으로 변경후 int를 암시적으로 T_20으로 형변환

(C++20~) 삼중 비교 연산자가 추가되어 !=, >, <=, >=deprecate 되었습니다.

삼중 비교 연산자 default 정의

삼중 비교 연산자default로 정의할 수 있습니다. 이때에는 ==도 컴파일러가 같이 정의해 줍니다.

컴파일러는 각 멤버 변수의 선언 순서대로 비교합니다. 이때 컴파일러에 따라 비교 속도 최적화를 위해 vector와 같은 컨테이너는 각 요소의 대소 비교 전에 요소 갯수에 대한 비교를 선행할 수 있습니다.

리턴 타입은 3개의 비교 카테고리(strong_ordering, weak_ordering, partial_ordering) 중 하나입니다. 컴파일러 판단을 그대로 따르기 위해 auto를 사용하기도 합니다.(리턴 타입 추론 참고)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T_20 {
    int m_Val;
public:
    explicit T_20(int val) : m_Val{val} {}
    
    auto operator <=>(const T_20& other) const = default; 
    // bool operator ==(const T_20& other) const {return m_Val == other.m_Val;} // default 로 정의하면, ==를 정의할 필요가 없습니다.
};  

EXPECT_TRUE(T_20{10} == T_20{10});  
EXPECT_TRUE(T_20{10} != T_20{20}); 
EXPECT_TRUE(T_20{10} < T_20{20}); 
EXPECT_TRUE(T_20{20} > T_20{10}); 
EXPECT_TRUE(T_20{10} <= T_20{20} && T_20{10} <= T_20{10}); 
EXPECT_TRUE(T_20{20} >= T_20{10} && T_20{10} >= T_20{10});

만약 ==, !=, <, >, <=, >=사용자 정의 버전과 <=> 버전이 혼용된다면, <=>으로 비교하면 <=> 버전을 사용하고, ==, !=, <, >, <=, >= 으로 사용하면, 사용자 정의 버전을 우선적으로 사용합니다. 단, != 경우 사용자 정의 버전이 없더라도 == 가 정의되어 있다면, ==을 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class T_20 {
    int m_Val;
public:
    explicit T_20(int val) : m_Val{val} {}
    
    auto operator <=>(const T_20& other) const = default; 
    bool operator <(const T_20& other) const { // 사용자 정의 버전입니다.
        std::cout << "T_20::operator <" << std::endl;
        return m_Val < other.m_Val;
    } 
    bool operator ==(const T_20& other) const { // 사용자 정의 버전입니다.
        std::cout << "T_20::operator ==" << std::endl;
        return m_Val == other.m_Val;
    }             
}; 

EXPECT_TRUE(T_20{10} < T_20{20}); // < 을 사용합니다. 
EXPECT_TRUE(T_20{20} > T_20{10}); // > 이 없으므로 <=> 을 사용합니다.
EXPECT_TRUE((T_20{10} <=> T_20{20}) < 0); //<=> 을 사용합니다.

EXPECT_TRUE(T_20{10} == T_20{10}); // == 을 사용합니다.
EXPECT_TRUE(T_20{10} != T_20{20}); // !=이 없으나 사용자 정의한 ==이 있으므로 == 을 사용합니다. 
EXPECT_TRUE((T_20{10} <=> T_20{10}) == 0); //<=> 을 사용합니다.

상등 비교와 동등 비교

삼중 비교 연산자는 기존의 비교 연산자처럼 bool를 리턴하는 것이 아니라 비교 카테고리(strong_ordering, weak_ordering, partial_ordering) 중 하나를 리턴합니다. 비교 카테고리를 알아보기 전에 먼저 상등 비교동등 비교의 개념을 알아 두어야 합니다.

대소 비교의 논리 조건에서 x < y 도 아니고 y < x 도 아니면, xy 와 동등하다는 논리를 말씀드렸는데요, 두개의 개체가 같은지를 비교하는 건 세부적으로 상등 비교동등 비교로 구분할 수 있습니다.

  • 상등 비교 : 두 개체의 데이터가 동일합니다.
  • 동등 비교 : 두 개체의 데이터가 개념적으로 동일합니다.

예를 들어 면적을 다루는 Area 개체를 생각해 봅시다. 가로 X 세로가 2 X 33 X 2 인 개체는 가로와 세로값이 달라 상등하지 않지만, 개념적으로 면적값은 6이므로 동등하다고 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Area {
    int m_Width;
    int m_Height;
public:
    Area(int width, int height) : m_Width{width}, m_Height{height} {}
    int GetData() const {return m_Width * m_Height;}

    // 상등 비교. 가로와 세로가 같은지 검사합니다.
    bool IsEqual(const Area& other) const {
        if (m_Width != other.m_Width) return false;
        return m_Height == other.m_Height;
    }

    // 동등 비교 : 개념적으로 같은지, 즉 면적이 같은지 검사합니다.
    bool IsEquivalence(const Area& other) const {
        return GetData() == other.GetData();
    }
};
Area a{2, 3};
Area b{3, 2};
EXPECT_TRUE(a.IsEqual(b) == false); // 2 X 3 과 3 X 2 는 상등하지 않습니다. 
EXPECT_TRUE(a.IsEquivalence(b) == true); // 2 X 3 과 3 X 2 는 동등합니다. 

== 구현시 상등 비교를 할 것인지, 동등 비교를 할 것인지는 전적으로 개발자의 판단에 따릅니다.

비교 카테고리와 삼중 비교 연산자의 리턴 타입

삼중 비교 연산자는 3개의 비교 카테고리 중 하나를 리턴합니다. 이 값을 확인하면 == 비교시 개체가 상등 비교를 지원하는지, 동등 비교를 지원하는지 좀더 쉽게 파악할 수 있습니다.

항목 내용
strong_ordering ==, !=, <, >, <=, >=의 비교 연산을 제공합니다. 여기서 ==상등 비교입니다. 즉 데이터들이 완전히 동일함을 의미합니다.
weak_ordering ==, !=, <, >, <=, >=의 비교 연산을 제공합니다. 여기서 ==동등 비교를 합니다. 즉, 개념적으로 동일함을 의미합니다. 예를들어 대소문자 구분없이 비교 할때 Aa아스키 코드값이 다르므로 상등하지 않지만, 개념적으로 동등합니다.
partial_ordering ==, !=, <, >, <=, >=의 비교 연산을 제공합니다. 실수 타입과 같이 대소 비교는 가능한데, ==는 소수점 오차등으로 상등 비교를 신뢰하기 애매한 경우입니다.

비교 카테고리의 포함 관계는 다음과 같습니다.

image

만약 클래스의 멤버 변수비교 카테고리들을 혼합해서 사용한다면, 삼중 비교 연산자는 상기 포함 관계 따라 가장 적은 것으로 결정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Strong_20 {
    int m_Val;
public:
    explicit Strong_20(int val) : m_Val{val} {}
    std::strong_ordering operator <=>(const Strong_20& other) const = default; 
};
class Weak_20 {
    int m_Val;
public:
    explicit Weak_20(int val) : m_Val{val} {}
    std::weak_ordering operator <=>(const Weak_20& other) const = default; 
};
class Partial_20 {
    int m_Val;
public:
    explicit Partial_20(int val) : m_Val{val} {}
    std::partial_ordering operator <=>(const Partial_20& other) const = default; 
};

class Mix_20 {
    Strong_20 m_Strong; // strong_ordering
    Weak_20 m_Weak; // weak_ordering
    Partial_20 m_Partial; // partial_ordering

public:
    explicit Mix_20(int strong, int weak, int partial) : m_Strong{strong}, m_Weak(weak), m_Partial(partial) {}
    auto operator <=>(const Mix_20& other) const = default; // partial ordering
};

std::partial_ordering result{Mix_20{0, 0, 0} <=> Mix_20{1, 1, 1}}; // partial_ordering을 리턴합니다.

(C++20~) 비트 쉬프트 연산자의 기본 비트 표준화

기존의 비트 쉬프트 연산자는 비록 표준에 정의되지는 않았으나, 양수던 음수이던, << 1은 곱하기 2의 효과가 있고, >> 1은 나누기 2의 효과가 있었는데요(비트 쉬프트 연산자 참고),

C++20부터는 비트 쉬프트 연산자의 기본 비트가 표준화되어 << 1는 곱하기 2의 효과가 있는 비트(즉, 0)로 채워지고, >> 1은 나누기 2의 효과가 있는 비트(즉, 양수면 0, 음수면 1)로 채워집니다.

태그:

카테고리:

업데이트:

댓글남기기