[Advanced C++] 57. 멤버 함수 오버로딩, 단항 연산자 오버로딩, 비교 연산자 오버로딩, spaceship operator <=>, 증감 연산자 오버로딩(전위, 후위)

dev.kelvin·2025년 5월 29일
1

Advanced C++

목록 보기
57/74
post-thumbnail

1. 멤버 함수 오버로딩

멤버 함수 오버로딩

연산자 오버로딩은 friend함수, 일반 함수(비멤버), 멤버 함수로 오버로딩이 가능하다

멤버 함수로 연산자 오버로딩에서 왼쪽 피연산자는 암시적으로 *this객체가 되고 다른 피연산자는 매개변수가 된다

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1, int inValue2, int inValue3) : value1{ inValue1 }, value2{ inValue2 }, value3{ inValue3 } {}

        Foo operator+(const Foo& other) const; //멤버함수로 연산자 오버로딩, 멤버 데이터 변경 방지를 위해 const함수로 선언

    private:
        int value1{};
        int value2{};
        int value3{};
    };

    Foo Foo::operator+(const Foo& other) const //연산자 오버로딩 멤버함수 정의
    {
        return Foo{ value1 + other.value1, value2 + other.value2, value3 + other.value3 };
    }

    int main() 
    {
        Foo f1{ 1, 2, 3 };
        Foo f2{ 4, 5, 6 };

        Foo f3{ f1 + f2 };

        return 0;
    }

f1 + f2는 f1.operator+(f2);로 변환되어 호출되는 방식이다 (왼쪽 피연산자를 명시하지 않고 *this로 암시적으로 처리했기 때문)

그렇다면 friend로 연산자 오버로딩을 하는것과 멤버함수로 연산자 오버로딩을 하는것중 어떤것을 선택해서 사용해야 할까?

우선 할당(=), 첨자([]), 함수호출(()), 멤버 선택(->)연산자는 반드시 멤버 함수로 연산자 오버로딩을 해야한다

a = b는 a.operator=(b);
arr[i]는 arr.operator;
func(a)는 func.operator()(a);
ptr->member는 ptr.operator->()->member;가 된다

하지만 operator <<는 멤버함수로 오버로딩 할 수 없다, 왜냐하면 왼쪽 피연산자로 std::ostream타입 객체가 들어가야 하기 때문이다

따라서 operator <<는 friend함수나 일반 함수로 오버로딩해야 한다
(일반적으로 왼쪽 피연산자가 클래스가 아니거나 (int와 같은), 수정할 수 없는 STL클래스와 같은 경우 (std::ostream)에는 멤버함수 오버로딩이 불가능하다 -> 왼쪽 피연산자가 *this로 자동으로 들어가야 하기 때문에 왼쪽 피연산자 클래스에서 구현해야 하기 때문, 근데 클래스를 수정할 수 없다면 사용할 수 없음)

보통 왼쪽 피연산자를 수정하지 않는 이항 연산자 ex) operator+와 같은 경우에는 일반 함수나 friend함수를 사용하여 오버로딩을 한다

이는 매개변수를 2개를 작성하기 때문에 조금 더 형태가 직관적일 수 있고 좌측 피연산자가 클래스가 아니거나 수정할 수 없는 STL클래스인 경우에도 사용할 수 있기 때문이다

왼쪽 피연산자를 수정해야 하는 경우 ex) operator+=와 같은 경우에는 멤버 함수를 이용하여 오버로딩을 한다
(왼쪽 피연산자는 항상 클래스 타입이 되며, 수정 가능한 클래스 타입이어야 한다, 수정할 수 없다면 friend나 일반함수를 사용해야 한다)

단항 연산자는 일반적으로 멤버함수로 오버로딩 하는게 좋다 ex) operator-, operator!
매개변수가 필요없어 깔끔하다
(-a는 a.operator-())

단 이때 후위 증감연산자는 구분을 위해 int 매개변수를 사용한다 (예외)


2. 단항 연산자 +, -, ! 오버로딩

위에서 단항 연산자(하나의 피연산자에만 작용하는 연산자)는 멤버 함수로 오버로딩 하는게 좋다고 정리했다 (연산자를 호출한 객체에만 작용하기 때문)

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1, int inValue2, int inValue3) : value1{ inValue1 }, value2{ inValue2 }, value3{ inValue3 } {}

        Foo operator-() const; //단항 연산자이기 때문에 멤버함수로 오버로딩

    private:
        int value1{};
        int value2{};
        int value3{};
    };

    Foo Foo::operator-() const
    {
        return Foo{ -value1, -value2, -value3 }; //전부 -로 바꾸고 Foo타입 임시객체로 반환
    }

    int main() 
    {
        Foo f1{ 1, 2, 3 };
        Foo f2{ 4, 5, 6 };

        Foo f3{ -f1 };

        return 0;
    }

a - b와 -a는 같은 연산자를 오버로딩 하지만 매개변수의 차이가 있기때문에 혼동할 일이 없다

이때 단항 연산자 +와 같은 경우에는 수학적 의미에서 값에 아무런 변화를 주지 않기 때문에 그냥 *this를 return해주면 된다

	Foo operator+() const;
    
    Foo Foo::operator+() const
    {
    	return *this;
    }

+단항 연산자를 오버로딩 하는 일은 거의 드물다

not연산자인 !를 오버로딩은 다음과 같이 처리한다 (true면 false로 false면 true로 변경)

	Foo operator!() const;
    
    bool Foo::operator!() const;
    {
    	return (value1 == 0 && value2 == 0 && value3 == 0);
    }

물론 구현부의 코드는 전부 예시이고 프로그래머가 상황에 맞게 구현하면 된다


3. 비교 연산자 오버로딩

비교 연산자 오버로딩

비교 연산자도 마찬가지로 좌측 피연산자를 수정하지 않는 연산자이기 때문에 friend나 일반함수로 오버로딩을 하는것이 좋다

operator==와 operator!=를 오버로딩 해보자

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1) : value1{ inValue1 } {}
		
        //비교 연산자 함수 오버로딩 선언 (friend)
        friend bool operator==(const Foo& f1, const Foo& f2);
        friend bool operator!=(const Foo& f1, const Foo& f2);

    private:
        int value1{};
    };

	//== 오버로딩
    bool operator==(const Foo& f1, const Foo& f2)
    {
        return f1.value1 == f2.value1;
    }

	//!= 오버로딩
    bool operator!=(const Foo& f1, const Foo& f2)
    {
        return !(f1 == f2); //operator==을 사용
    }

    int main() 
    {
        Foo f1{ 1 };
        Foo f2{ 4 };

        bool bIsSame{ f1 == f2 }; //false
        bool bIsDiff{ f1 != f2 }; //true

        return 0;
    }

그렇다면 opearator>와 operator<는 어떨까? (<=, >=도 마찬가지)

일반적으로 클래스타입끼리의 operator>와 <는 직관적이지 않기때문에 잘 사용하지 않지만 비교해야하는 경우가 생길 수 있다

예를들면 std::sort나 std::map, std::set과 같은 컨테이너는 내부 element를 비교하기 위해 operator<를 사용한다

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1) : value1{ inValue1 } {}

        friend bool operator<(const Foo& f1, const Foo& f2);
        friend bool operator>(const Foo& f1, const Foo& f2);

    private:
        int value1{};
    };

    bool operator<(const Foo& f1, const Foo& f2)
    {
        return f1.value1 < f2.value1;
    }

    bool operator>(const Foo& f1, const Foo& f2)
    {
        return f1.value1 > f2.value1;
    }

    int main() 
    {
        Foo f1{ 1 };
        Foo f2{ 4 };

        f1 > f2;
        f1 < f2;

        return 0;
    }

마찬가지로 결과값은 bool이기 때문에 반환형을 bool로 하고 두개의 피연산자를 대상으로 하며 왼쪽 피연산자가 수정되지 않기 때문에 friend함수로 오버로딩을 한 코드이다

위에서 정리한 ==, >, <는 각 구현이 굉장히 유사하다, 따라서 중복을 피하기 위해 기존 연산자 오버로딩 함수를 이용할 수 있다

    bool operator==(const Foo& f1, const Foo& f2)
    {
        return f1.value1 == f2.value1;
    }

    bool operator!=(const Foo& f1, const Foo& f2)
    {
        return !(f1 == f2);
    }

    bool operator<(const Foo& f1, const Foo& f2)
    {
        return f1.value1 < f2.value1;
    }

    bool operator>(const Foo& f1, const Foo& f2)
    {
        return !(f1 < f2);
    }

    int main() 
    {
        Foo f1{ 1 };
        Foo f2{ 4 };

        bool result1{ f1 > f2 };
        bool result2{ f1 < f2 };
        bool result3{ f1 == f2 };
        bool result4{ f1 != f2 };

        return 0;
    }

spaceship operator <=>

spaceship operator <=>는 이러한 비교 연산자들을 한번에 오버로딩 할 수 있는 연산자이다 (C++20)

    #include <compare>

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1) : value1{ inValue1 } {}

		//auto로 반환형을 컴파일러가 알아서 추론하도록 만들고 default를 사용 (변수를 순서대로 비교)
        auto operator<=>(const Foo& other) const = default;

    private:
        int value1{};
    };

    int main() 
    {
        Foo f1{ 1 };
        Foo f2{ 4 };

		//따로 연산자 오버로딩을 하지 않아도 <=> 하나만으로 전부 사용이 가능하다
        bool result1{ f1 > f2 };
        bool result2{ f1 < f2 };
        bool result3{ f1 == f2 };
        bool result4{ f1 != f2 };

        return 0;
    }

이때 내부 변수를 순서대로 비교하지 않고 operator<=>를 직접 구현하고 싶다면 다음과 같이 처리한다

	#include <compare> //std::strong_ordering
    
    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1) : value1{ inValue1 } {}

        std::strong_ordering operator<=>(const Foo& other) const;

    private:
        int value1{};
    };

    std::strong_ordering Foo::operator<=>(const Foo& other) const
    {
    	//원하는대로 구현 (예시임)
        if (value1 < other.value1)
        {
            return std::strong_ordering::less;
        }
        else if (value1 > other.value1)
        {
            return std::strong_ordering::greater;
        }

        return std::strong_ordering::equal;
    }

    int main()
    {
        Foo f1{ 4 };
        Foo f2{ 4 };

        std::strong_ordering result{ f1 <=> f2 };

        return 0;
    }

<=>연산자는 std::string_ordering타입의 객체를 return한다 이 클래스에는 less, greater, equal이 있어 비교가 가능하다


4. 증감 연산자 오버로딩

증감 연산자 오버로딩

증감 연산자 오버로딩에는 전위(prefix)인지 후위(postfix)인지를 구분해야 한다 (++a, a++)

증감 연산자는 모두 단항 연산자이고 피연산자를 수정하기 때문에 멤버함수로 오버로딩 하는걸 권장한다

전위 증감 연산자 오버로딩부터 정리해보자

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1) : value1{ inValue1 } {}

        Foo& operator++();
        Foo& operator--();

        friend std::ostream& operator<<(std::ostream& os, const Foo& inFoo);

    private:
        int value1{};
    };

    Foo& Foo::operator++()
    {
        ++value1;
        return *this;
    }

    Foo& Foo::operator--()
    {
        --value1;
        return *this;
    }

    std::ostream& operator<<(std::ostream& os, const Foo& inFoo)
    {
        os << inFoo.value1;
        return os;
    }

    int main()
    {
        Foo f1{ 4 };

        std::cout << ++f1;

        return 0;
    }

전위 증감 연산자 오버로딩에서 중요한점은 반환형이 자기자신 클래스 타입의 참조이고 *this를 return한다는 점이다

이는 chain을 가능하게 하기 위함이다 (자기자신 객체를 참조로 반환하여 다른곳에도 이어서 사용할 수 있게 하기 위함)

후위 증감 연산자 오버로딩은 자기자신 객체를 참조가 아닌 값으로 반환하고 매개변수로 int가 들어가야 한다

    class Foo
    {
    public:
        Foo() {};
        Foo(int inValue1) : value1{ inValue1 } {}

        Foo operator++(int);
        Foo operator--(int);

        Foo& operator++();
        Foo& operator--();

        friend std::ostream& operator<<(std::ostream& os, const Foo& inFoo);

    private:
        int value1{};
    };

    Foo& Foo::operator++()
    {
        ++value1;
        return *this;
    }

    Foo& Foo::operator--()
    {
        --value1;
        return *this;
    }

	//값 타입 반환, 매개변수 int
    Foo Foo::operator++(int)
    {
        Foo f{ *this };
        ++(*this); // 전위 증감 연산자 오버로딩 함수 사용

        return f;
    }

	//값 타입 반환, 매개변수 int
    Foo Foo::operator--(int)
    {
        Foo f{ *this };
        --(*this); // 전위 증감 연산자 오버로딩 함수 사용

        return f;
    }

    std::ostream& operator<<(std::ostream& os, const Foo& inFoo)
    {
        os << inFoo.value1;
        return os;
    }

    int main()
    {
        Foo f1{ 4 };

        std::cout << f1++;

        return 0;
    }

operator++,--는 반환형과 매개변수로 구분을 하여 오버로딩을 한다

이때 전위 증감 연산자는 chain해서 계속 사용할 수 있게 하기 위해서 참조타입을 반환하고 후위 증감 연산자는 그렇지 않기 때문에 값 타입으로 반환하고 매개변수 int가 들어간다

후위 증감 연산자는 증가되기 전 값을 return하고 그 다음에 값을 증가시켜야 하기 때문에 local variable에 자기 자신의 값을 캐싱하고 그 값을 return한다, 이때 실제로 자기자신 객체의 값을 증감시킨다 (*this를 이용하여)

후위 증감 연산자의 반환형이 참조가 아닌 이유가 바로 여기서 나온다, local variable 임시 변수를 return해야 하기 때문이다

전위 증감 연산자는 이러한 임시 변수 생성 및 캐싱 작업이 없기 때문에 후위 증감 연산보다 오버헤드가 적다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글