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

Jangmanbo·2023년 3월 21일
0

Effective C++

목록 보기
11/33

자기대입
어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것

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

언뜻 봐서는 이런 자기 대입 코드를 누가 쓰겠냐 싶지만 많은 사용자들이 이런 코드를 작성하게 된다.

자기대입의 가능성이 있는 코드 예시

a[i] = a[j];	// i와 j가 같은 값을 가지면
*px = *py;		// px와 py가 같은 대상을 가리키면

이러한 자기대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태인 중복참조(aliasing) 때문이다.
따라서 같은 타입으로 만들어진 객체 여러개를 참조자 혹은 포인터로 물어놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성으로 고려해야 한다.


같은 클래스 계통에서 만들어진 객체라 해도 굳이 동일한 타입으로 선언할 필요는 없다.
파생 클래스 타입의 객체를 참조하거나 가리키는 용도로 기본 클래스의 참조자나 포인터를 사용하면 되기 때문이다.

class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, Derived* pd);	// rb와 pd는 같은 객체였을 수도 있다.

객체를 복사하기 위해 대입 연산자를 사용할 때는 사용자가 신경쓰지 않아도 자기대입에 대해 안전하게 동작하도록 해야 한다. 다음은 자기대입에 대해 안전하지 않은 코드이다.
class Bitmap { ... };
class Widget {
	...
private:
   Bitmap *pb;	// 힙에 할당한 객체를 가리키는 포인터
}

// 안전하지 않은 대입 연산자
Widget& Widget::operator=(const Widget& rhs)
{
	delete pb;	// 현재의 비트맵 사용을 중단하고
    pb = new Bitmap(*rhs.pb);	// rhs의 비트맵을 사용하도록 만든다.
    
    return *this;	// 항목10 참조
}

*thisrhs는 같은 객체일 가능성이 있다. 만약 같은 객체일 경우 delete 연산자가 *this 객체의 비트맵에만 적용되는 것이 아니라 rhs 객체까지 적용된다.

따라서 이 함수가 끝나는 시점에 해당 Widget 객체는 자신의 포인터 멤버를 통해 물고 있던 객체가 삭제된 상태일 것이다.


해결법1: 일치성 검사

전통적인 해결법은 operator=의 첫머리에서 일치성 검사(identity test)를 통해 자기대입을 점검하는 것이다.

Widget& Widget::operator=(const Widget& rhs)
{
	if (this == &rhs) return *this;	// 일치성 테스트. 객체가 같은지(자기대입인지) 검사
    
    // 아까와 같은 코드...
	delete pb;	// 현재의 비트맵 사용을 중단하고
    pb = new Bitmap(*rhs.pb);	// rhs의 비트맵을 사용하도록 만든다.
    
    return *this;	// 항목10 참조
}

그러나 이 방법으로도 아직 예외 안정성에 대해서는 문제가 남아있다.

특히 new Bitmap 표현식에서 예외가 터지게 되면(ex. 동적 할당에 필요한 메모리 부족, Bitmap 클래스 복사 생성자에서의 예외 등) Widget 객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가지게 된다.

삭제된 객체를 가리키는 포인터는 delete 연산자를 안전하게 적용할 수도, 안전하게 읽는 것도 불가능하다.


해결법2: 문장 순서 바꾸기

예외 안정성에 집중하면 자기대입 문제는 무시하더라도 무사히 넘어갈 확률이 높아진다.

Widget& Widget::operator=(const Widget& rhs)
{
	Bitmap *pOrig = pb;	// 기존 pb를 어딘가에 저장
    pb = new Bitmap(*rhs.pb);	// pb가 *pb의 사본을 가리키게 함
    delete pOrig;	// 현재의 pb 삭제
    
    return *this;
}

무턱대고 pb를 삭제하지 말고 이 포인터가 가리키는 객체를 복사한 직후에 삭제한다.
문장 순서를 바꾸는 것만으로도 예외 안정성과 자기대입에 대해 안전한 코드가 만들어졌다!


해결법3: 복사 후 맞바꾸기(copy and swap)

해결법2처럼 예외 안정성과 자기대입 안정성을 모두 가진 대입 연산자를 구현하는 방법으로는 복사 후 맞바꾸기 기법도 있다.

class Widget {
  ...
  void swap(Widget& rhs);	// *this의 데이터와 rhs의 데이터 맞바꾸기 (항목 29)
  ...
}

Widget& Widget::operator=(const Widget& rhs)
{
	Widget temp(rhs);	// rhs의 데이터에 대해 사본 만들기
    swap(temp);			// *this의 데이터를 사본과 맞바꾸기
    
    return *this;
}

다르게 구현해보기

Widget& Widget::operator=(const Widget rhs)	// 기존 객체의 사본!
{
    swap(rhs);			// *this의 데이터를 사본의 데이터와 맞바꾸기
    
    return *this;
}
  1. 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언 가능
  2. 값에 의한 전달을 수행하면 전달된 대상의 사본이 생성 (항목 20)

이러한 C++의 두 가지 특징을 통해 조금 다르게 구현해볼 수도 있다.


정리) 대입 연산자가 자기 대입에 대한 안정성을 갖기 위한 3가지 방법
1. 일치성 검사
2. 문장 순서 바꾸기
3. 복사 후 맞바꾸기

0개의 댓글