본문 바로가기
Book & Lecture/Effective C++

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

by studio ODOC 2023. 2. 18.
반응형

Effective C++ 항목11

operator에서는 자기대입에 대한 처리가 빠지지 않도록 하자

 

선 요약

  1. operator=함수를 구현할 때, 자기대입을 제대로 처리하도록 하자.
    1. 객체간의 주소 비교
    2. 호출 순서를 적절히 조절
    3. "복사 후 맞바꾸기" 기법
  2. 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인지 정확하게 체크하자.

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

위와 같이 C++에서는 자기 자신을 대입하는 자기대입이 적법(legal)하다.

자기대입은 여러 곳에서 하나의 객체를 참조하는 상태, 즉 중복참조라고도 불린다.

 

중복참조란?

class Base { ... }
class Derived { ... }

void doSomething(const Base& rb, Derived* pd); // rb 와 *pd는 같은 객체를 가리키고 있을 수도..?

위와 같이 기본 클래스 rb와 파생 클래스 pd가 같은 객체를 가리키고 있을 수도 있다.

 

이때, 대입 연산자를 조심하여 사용해야한다. 자기대입에 대해 안전하게 동작해야 한다!

 

 

중복참조의 문제 사례

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

Widget& Widget::operator=(const Widget& rhs) // 중복참조에 대해 안전하지 않음
{
  delete pb;
  pb = new Bitmap(*rhs.pb); // 여기서 예외가 발생한다면..?
  
  return *this;
}

위의 코드에서 new Bitmap 과정에 예외가 발생한다면?

Widget이 멤버로 갖는 pb는 아무것도 가리키지 않게 되고, 이제부터 operator=함수는 정상적으로 동작할 수 없게되는 커다란 문제가 생긴다!

 

중복참조 해결 방법

<해결책 1>

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

위와 같이 객체가 같은지 체크하여, 중복참조라면 함수를 리턴해버릴 수 있지만

하지만 마찬가지로 new Bitmap에서 예외가 발생하면 기존 문제가 동일하게 발생한다.

그리고 분기문을 사용하면 프로그램 성능이 저하되므로 다른 방법을 살펴보자.

 

<해결책2>

Widget& Widget::operator=(const Widget& rhs)
{
  Bitmap* pOrig = pb; // 원래의 pb를 백업
  pb = new Bitmap(*rhs.pb); // pb가 rhs.pb의 복사본을 가리킴
  delete pOrig; // 그 다음 원래의 pb삭제
  
  return *this;
}

해결책1과 달리 분기문을 사용하지 않아 성능 저하도 없고,

new Bitmap에서 예외가 발생하더라도 pb는 변경되지 않은 상태를 유지할 수 있다.

 

<해결책3-복사 후 맞바꾸기(copy and swap)>

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

Widget& Widget::operator=(const Widget& rhs)
{
  Widget temp(rhs); // rhs의 복사본 생성
  swap(temp); // *this의 데이터와 복사본의 데이터 맞바꿈(swap)
  return *this;
}

1.클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다는 점

2.값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다는 점(항목20)

위 2가지 C++특징을 이용하는 해결책이다.

 

여기서 operator=함수 내의 복사본 생성 과정을 생략하여 코드를 간결하게 만들 수 있다.

바로 인자를 처음부터 복사생성된 객체로 받는 것이다. (call by value 이용)

Widget& Widget::operator=(Widget rhs) // rhs 에 대해 복사 생성자가 호출됨
{
  swap(rhs);
  return *this;
}
반응형