C++/C++ 심화 강좌

[C++ 심화 강좌] C++20 <=> Three-way 비교 연산자

카루-R 2021. 9. 22. 18:00
반응형

환영합니다, Rolling Ress의 카루입니다.

사실 작성한 건 3월...? 정도인데, 어쩌다가 이제서야 올리는지...아무튼 시작하겠습니다.

드디어 이걸 포스팅하는 날이 오네요. 제가 이해하는데도 상당한 시간이 걸린, Three-way Comparsion Operator <=> 입니다. 이건 다른 것보다 클래스에서 비교 연산자를 만들 때 특히 도움이 되겠더라고요. <, >, <=, >=, ==, != 정의할 필요 없이 auto operator<=> 한방이면 끝납니다.

일단 이게 무엇인지는 알아야겠죠. 그 전에, 중학교 3학년 수학에서 배우셨을 법한 실수의 대소비교에 대해서 알아보도록 하겠습니다.

두 실수 a, b에 대해 반드시 위 세 가지 중 하나가 성립하죠. 한 실수에서 다른 실수를 뺀 값을 0과 비교하면 크기를 알 수 있습니다. 이게 C++20에서도 똑같이 적용이 됩니다.

#include <iostream>
#include <compare>

int main()
{
    auto n{ 10 }, m{ 20 };
    auto result{ n <=> m };

    if (result < 0)
        std::cout << "n < m" << std::endl;
    else if (result > 0)
        std::cout << "n > m" << std::endl;
    else if (result == 0)
        std::cout << "n == m" << std::endl;
}

출력결과: n < m

 

n, m에 각각 10, 20을 대입하고 result에는 n <=> m의 결괏값을 담았습니다. result에 뭔 값이 담겼는지는 모르지만, 이걸 0과 비교하면 n과 m의 대소 관계를 알아낼 수 있어요.

여기서 궁금한 점이 생기죠. result의 형식은 무엇이냐? RTTI를 켜고 알아보죠.

 

std::cout << typeid(result).name() << std::endl;

출력결과: struct std::strong_ordering

 

strong_ordering이라는 구조체입니다. 참고로 이런 비교 구조체가 strong_ordering 말고도 weak_ordering, partial_ordering 등이 있는데, 정수끼리의 비교시엔 strong_ordering이 반환됩니다. 실수일 때는 partial_ordering이 반환되고요. 이제 타입을 알았으니, 이렇게 쓸 수도 있겠죠.

 

auto n{ 10 }, m{ 20 };
auto result{ n <=> m };

if (result == std::strong_ordering::less)
    std::cout << "n < m" << std::endl;
else if (result == std::strong_ordering::greater)
    std::cout << "n > m" << std::endl;
else if (result == std::strong_ordering::equal)
    std::cout << "n == m" << std::endl;

 

결과는 같습니다. 참고로 이게 클래스 멤버랑 관련이 되면서 좀 복잡해지면 weak_ordering이 반환되는 경우도 있는데, 사용법은 거의 동일해요. equal이 equivalent로 바뀔 뿐. 잘 모를 때는 그냥 0과 비교하면 됩니다.

이해가 안 된다면, C에서의 strcmp(), memcmp()를 떠올려보세요. 사용법은 정확히 동일합니다.

이게 빛을 발하는 부분은 클래스에서 사용할 때입니다. 다음 코드를 보세요.

 

#include <iostream>
#include <compare>

class Data {
public:
    int var1;
    int var2;
    auto operator<=>(const Data&) const = default;
};

int main()
{
    Data d1{ 10, 20 }, d2{ 30, 40 };

    if (d1 < d2) // <=, >, >=, ==, !=, <=> 모두 사용 가능
        std::cout << "d1 < d2" << std::endl;
}

출력결과: d1 < d2

 

클래스에다가는 <=> 연산자 하나만 정의해두면 됩니다. 그럼 컴파일러가 알아서 <, <=, >, >=, ==, != 연산자를 모두 구현해줘요. 내부적으로는 아까 봤던 ~_ordering 타입을 사용합니다. 그렇습니다. 이 연산자의 진짜 의미는 오버로딩에 있습니다. 조금 더 예시를 구체적으로 들어보죠. 클래스를 정의할 때, 다음과 같이 연산자들을 오버로딩합니다.

 

class MyClass
{
public:
    MyClass();

    bool operator==(const MyClass& rhs); //선언만, 정의는 별도
    bool operator!=(const MyClass& rhs);
    bool operator<(const MyClass& rhs);
    bool operator>(const MyClass& rhs);
    bool operator<=(const MyClass& rhs);
    bool operator>=(const MyClass& rhs);
}

 

토나오죠. 아니 어차피 하나 정의하면 다른 건 충분히 구현하지 않을까요? 당장 ==, !=만 봐도, 하나만 구현한 뒤 ! (NOT) 연산 처리하면 될 일입니다. 굳이 이런 곳에서 힘을 뺄 필요가 없죠. 그래서, 갑니다. 어차피 뻔한 비교연산자, 그냥 이 한 방에 해결하는 겁니다.

 

class MyNewClass
{
public:
    auto operator<=>(const MyNewClass&) const = default;
}

 

깔끔하죠. 심지어 위 MyClass 같은 경우에는 그냥 함수 선언만 했지, 정의는 하지 않았습니다. 즉, 사용자가 따로 정의를 해줘야 한다는 것이죠. 반대로, MyNewClass의 연산자는 이미 정의가 되어있습니다. 어디서요? = default; 에서요. 저렇게 하면 컴파일러가 알아서 '자동으로' 코드를 만들어줍니다. C++20의 막강한 기능 중 하나죠.

사용은 어떻게 하냐고요? 그냥 저렇게 해두면 ==, <, >, != 등등 모든 비교연산자를 사용할 수 있습니다. 모던 C++의 장점입니다.

반응형