C++ 대입 연산자 오버로딩 가이드

마이클의 AI 연구소·2022년 3월 16일
0

개요

C++의 한가지 멋진 기능 중 하나는 클래스를 선언할 때 연산자에 특별한 의미를 부여할 수 있다는 것이다. 이것은 operator overloading이라고 부른다. 아래 나열되는 특정 네이밍 컨벤션을 따라 클래스에 멤버함수를 선언함으로서 C++ operator overload를 구현할 수 있다. 예를들어 + 연산자를 오버로딩하기 위해서 클래스에 operator+라는 이름의 멤버함수를 만들어주면 된다.

일반적으로 오버로딩에 많이 쓰이는 연산자는 다음과 같다.

  • = (대입 연산자)
  • +, -, * (이진 산술 연산자)
  • +=, -=, *= (복합 대입 연산자)
  • ==, != (비교 연산자)

대입 연산자 =

대입 연산자는 다음과 같이 구현한다.

class MyClass {
public:
	...
    MyClass & operator=(const MyClass &rhs);
    ...
}

MyClass a, b;
...
b = a;  // b.operator=(a); 와 같은 의미

= 연산자는 대입의 우항에 상수참조를 받는 것을 확인할 수 있다. 우항은 변경되지 않고 오직 좌항만 변경하는 것을 보장하도록 구현하기 위함이다.

또한, 연산자 반환은 참조로 하는 것을 확인할 수 있다. 이는 다음과 같이 operator chaining을 허용하기 위함이다.

int a, b, c, d, e;
a = b = c = d = e = 42;

이는 컴파일러에 의해 다음과 같이 해석된다.

a = (b = (c = (d = (e = 42))));

다르게 말해서, 대입(할당)은 우측결합이다. 마지막 대입 연산을 먼저 계산하고, 좌측으로 연산하며 값이 전달되는 방식이다.

operator chaining을 지원하기 위해 대입 연산자는 반드시 어떤 값을 반환해야 한다. 반드시 반환되어야 하는 값은 좌항의 참조이다.

여기서 반환하는 참조는 const로 선언되어서는 안된다. 왜냐하면 다음과 같은 코드도 작성 가능하기 때문이다.

MyClass a, b, c;
...
(a = b) = c; // What??

따라서 가상의 MyClass의 대입 연산자를 다음과 같이 구현할 수 있다.

  // Take a const-reference to the right-hand side of the assignment.
  // Return a non-const reference to the left-hand side.
  MyClass& MyClass::operator=(const MyClass &rhs) {
    ...  // Do the assignment operation!

    return *this;  // Return a reference to myself.
  }

this는 멤버함수가 호출되고 있는 오브젝트의 포인터이다. a = ba.operator=(b)로 간주할 수 있다. 따라서 참조자를 반환해야 하므로 *this를 반환한다.

자기할당 체크

새로 정의하는 클래스 내에서 메모리할당을 한다면 중요한 것을 기억해야 한다. 일반적으로 대입연산자 함수 내에서는 다음과 같은 흐름으로 동작한다.

MyClass& MyClass::operator=(const MyClass &rhs) {
    // 1.  MyClass 내에서 사용중인 메모리를 해제하고 
    // 2.  대입해오는 우항의 값을 저장할 메모리를 새롭게 할당한다.
    // 3.  우항의 값을 새로 할당한 메모리 공간에 복사한다.
    // 4.  *this 를 반환한다.
  }

그런데 아래와 같은 코드를 작성한다면 어떻게 동작할까?

MyClass mc;
...
mc = mc;  // 헉

이 코드가 실행되는 순간 대혼란이 발생할 것이다. 왜냐하면 mc는 좌항이면서 우항이므로 좌항에서 메모리를 해제한 순간 복사할 원본의 메모리도 해제가 된 것이다. 따라서 어떤 값이 복사될지 알 수 없다.

여기서 자기할당을 체크하는 방법을 알아보자. 두 인스턴스가 완전히 동일한가 체크할 필요없이 두 객체의 주소가 같은지를 보면 된다. 주소가 같다면 대입을 하지 않으면 된다.

수정된 안전한 버전의 MyClass 클래스를 살펴보자.

  MyClass& MyClass::operator=(const MyClass &rhs) {
    // Check for self-assignment!
    if (this == &rhs)      // Same object?
      return *this;        // Yes, so skip assignment, and just return *this.

    ... // Deallocate, allocate new space, copy values...

    return *this;
  }

정리해보자.
1. 매개변수(우항)로 상수 참조를 취하라.
2. operator chaining을 지원하기 위해 좌항의 참조를 반환하라. (*this)
3. 포인터를 비교하여 자기할당을 체크하라. (this vs &rhs)

복합 할당 연산자

  MyClass a, b;
  ...
  a += b;    // Same as a.operator+=(b)

위와 같이 복합 할당 연산을 수행할 수 있다.

대입 연산자와 마찬가지로 다음과 같이 참조를 반환하도록 한다.

  MyClass & MyClass::operator+=(const MyClass &rhs) {
    ...   // Do the compound assignment work.

    return *this;
  }

이진 산술 연산자

복합 할당 연산자가 구현되어 있는 경우 복합 할당 연산자를 이용해서 이진 산술 연산자를 구현할 수 있다.

  // Add this instance's value to other, and return a new instance
  // with the result.
  const MyClass MyClass::operator+(const MyClass &other) const {
    MyClass result = *this;     // Make a copy of myself.  Same as MyClass result(*this);
    result += other;            // Use += to add other to the copy.
    return result;              // All done!
  }

조금 더 심플하게 구현한다면 다음과 같다.

// 인스턴스의 값을 다른 곳에 추가하고 결과와 함께 새 인스턴스를 반환합니다.
const MyClass MyClass::operator+(const MyClass &other) const {
    return MyClass(*this) += other;
}
profile
늘 성장을 꿈꾸는 자들을 위한 블로그입니다.

0개의 댓글