
멤버 함수 오버로딩
연산자 오버로딩은 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 매개변수를 사용한다 (예외)
위에서 단항 연산자(하나의 피연산자에만 작용하는 연산자)는 멤버 함수로 오버로딩 하는게 좋다고 정리했다 (연산자를 호출한 객체에만 작용하기 때문)
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);
}
물론 구현부의 코드는 전부 예시이고 프로그래머가 상황에 맞게 구현하면 된다
비교 연산자 오버로딩
비교 연산자도 마찬가지로 좌측 피연산자를 수정하지 않는 연산자이기 때문에 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이 있어 비교가 가능하다
증감 연산자 오버로딩
증감 연산자 오버로딩에는 전위(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해야 하기 때문이다
전위 증감 연산자는 이러한 임시 변수 생성 및 캐싱 작업이 없기 때문에 후위 증감 연산보다 오버헤드가 적다