#25. [모던 C++] 개선된 연산자(삼중 비교 연산자)(C++20)
- (C++20~) 삼중 비교 연산자가 추가되어 비교 연산자 구현이 간소화 되었습니다.
- (C++20~) 삼중 비교 연산자를 default로 정의할 수 있습니다.
- (C++20~) 비트 쉬프트 연산자의 기본 비트가 표준화되어
<< 1
는 곱하기 2의 효과가 있는 비트(즉,0
)로 채워지고,>> 1
은 나누기 2의 효과가 있는 비트(즉, 양수면0
, 음수면1
)로 채워집니다.
(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
또한 삼중 비교 연산자는 이종 타입과의 비교를 지원합니다.
이전에는 이종 타입과의 비교를 위해 케이스마다 함수 구현을 해야 했습니다. 다음 T
는 int
와 비교하기 위해 무려 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 < b
는 b > a
입니다.)을 고려하여 (right <=> left) > 0
로 변환하여 비교합니다. 즉, 왼쪽 인수와 오른쪽 인수를 바꿔서도 비교하므로, 비멤버 버전을 만들 필요가 없습니다.
따라서, int
를 인자로 받는 삼중 비교 연산자와 ==
멤버 버전만 추가하면 T_20
과 int
를 비교할 수 있습니다.
-
(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_20
이 explicit를 사용하지 않아 암시적으로 형변환 된다면, 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
도 아니면, x
는 y
와 동등하다는 논리를 말씀드렸는데요, 두개의 개체가 같은지를 비교하는 건 세부적으로 상등 비교와 동등 비교로 구분할 수 있습니다.
예를 들어 면적을 다루는 Area
개체를 생각해 봅시다. 가로 X 세로가 2 X 3
과 3 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 | == , != , < , > , <= , >= 의 비교 연산을 제공합니다. 여기서 == 은 동등 비교를 합니다. 즉, 개념적으로 동일함을 의미합니다. 예를들어 대소문자 구분없이 비교 할때 A 와 a 는 아스키 코드값이 다르므로 상등하지 않지만, 개념적으로 동등합니다. |
partial_ordering | == , != , < , > , <= , >= 의 비교 연산을 제공합니다. 실수 타입과 같이 대소 비교는 가능한데, == 는 소수점 오차등으로 상등 비교를 신뢰하기 애매한 경우입니다. |
비교 카테고리의 포함 관계는 다음과 같습니다.
만약 클래스의 멤버 변수가 비교 카테고리들을 혼합해서 사용한다면, 삼중 비교 연산자는 상기 포함 관계 따라 가장 적은 것으로 결정합니다.
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
)로 채워집니다.
댓글남기기