KoreanFoodie's Study

Effective C++ | 항목 11 : operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 11 : operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자

GoldGiver 2022. 10. 25. 16:07

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 11 : operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자

핵심 :

1. operator= 를 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들자. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 된다.
2. 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인하자.

 

이런 코드는 사실 적법한(legal) 코드이다.

class Widget{ ... }
Widget w;
...
w = w // 자기 자신 대입

사실 자기 자신을 대입할 수 있는 가능성을 가진 코드는 생각보다 종류가 다양하다.

// 가리키는 것이 같다면...
a[i] = a[j];
*px = *py;

// 다른 타입이지만 실제로는 같은 경우
class Base { ... }
class Derived { ... }
// rb 와 *pd 가 같은 객체라면?
void doSomething(const Base& rb, Derived* pd);

위와 같은 경우에도, 자기 대입은 안전하게 동작해야 한다. 예시를 보자.

class Widget
{
  ...
  private:
    Bitmap* pb;
};

Widget& Widget::operator=(const Widget& rhs)
{
  delete pb;
  pb = new Bitmap(*rhs.pb);
  
  return *this;
}

위의 예시에서, 만약 rhs 가 자기 자신이면 어떻게 될까? delete 연산자가 *this 객체의 비트맵에만 적용되는 것이 아니라, rhs 의 객체까지 적용되어 버린다.

간단하게 인자가 자기 자신인지를 체크하는 코드를 삽입해 보자.

Widget& Widget::operator=(const Widget& rhs)
{
  if (this == &rhs) return *this;
  delete pb;
  pb = new Bitmap(*rhs.pb);
  
  return *this;
}

위와 같이 처리하면 잘 될 것 같지만, 사실 이 코드는 예외에 안전하지 않다. 만약 아래에 있는 'new Bitmap' 라인에서 예외가 발생하면, pb 는 삭제된 상태로 남게 되어, 삭제된 Bitmap 을 가리키는 포인터를 계속 가지게 된다.

코드의 순서를 조금 바꾸면, 위에서 언급한 위험성을 어느 정도 해소할 수 있다.

Widget& Widget::operator=(const Widget& rhs)
{
  Bitmap* pOrig = pb;
  pb = new Bitmap(*rhs.pb);
  delete pOrig;
  
  return *this;
}

이 코드는 이제 예외에 안전하다. 'new Bitmap' 부분에서 예외가 발생해도 pb 는 변경되지 않은 상태가 유지되기 때문이다. 또한 일치성 테스트는 분기문을 만들어 성능을 저하시키는데, 그 문제에 있어서도 자유롭다.

 

또 다른 방법으로는, '복사 후 맞바꾸기(copy and swap)' 이 있다. 예시를 보자.

class Widget
{
  // *this 의 데이터 및 rhs의 데이터를 맞바꾼다
  void swap(Widget& rhs);
};

Widget& Widget::operator=(const Widget& rhs)
{
  Widget temp(rhs);
  swap(temp);  
  return *this;
}

이 방법은 C++ 가 '클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다는 점' 과, '값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다는 점' 을 이용하여 조금 다르게 구현할 수도 있다.

// rhs 는 원래 객체의 '사본' 이다
// 즉, rhs 에 대해 복사 생성자가 불렸다고 보면 된다
Widget& Widget::operator=(Widget rhs)
{
  swap(rhs);
  return *this;
}

위 코드는 명확성을 제물로 바쳤지만, 객체를 복사하는 코드가 함수 본문으로부터 매개변수의 생성자로 옮겨졌기 때문에, 컴파일러가 더 효율적인 코드를 생성할 수 있는 여지가 만들어 졌다!

Comments