- operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리해야 한다.
원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며 복사 후 맞바꾸어도 된다. - 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 같은 객체인 경우에 정확하게 동작하게 해라.
operator= 자기대입 처리
자기대입이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말한다.
class Widget { ... };
Widget w;
...
w = w; // 자기대입
이 코드는 문제가 없는 적법한 코드이다.
다음과 같은 상황에 많이 발생한다.
a[i] = a[j]; // i==j라면 자기대입
*px = *py; // 가리키는 대상이 같으면 자기대입
이러한 자기대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태, 중복참조 때문이다.
같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려해야 한다.
또한, 파생 클래스를 기본 클래스 타입의 참조자나 포인터를 사용해서 가리킬 수 있기 때문에 이 부분도 주의해야 한다.
class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, Derived* pd); // rb, *pd는 같은 객체일 수 있음
자기대입 해결법
자원 관리 용도로 객체를 만들어야 하는 상황에서, 자원 관리 객체들이 복사될 때 올바르게 동작하게 만드는 상황을 가정해 보자. 이 상황에서 대입 연산자가 자기대입에 대해 안전하게 동작하게 해야 한다.
동적 할당된 비트맵을 가리키는 원시 포인터를 데이터 멤버로 갖는 클래스를 만들었다고 가정해보자.
class Bitmap { ... };
class Widget{
...
Widget& operator=(const Widget& rhs);
private:
Bitmap *pb; // 힙에 할당된 객체를 가리키는 포인터
};
Widget& Widget::operator=(const Widget& rhs) // 안전하지 않은 operator=
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
위와 같이 operator=를 구현한다면 자기대입에 대한 위험이 있는 코드이다.
만약 rhs와 *this가 같은 객체라면 delete 연산자가 *this 객체의 비트맵에만 적용되는 것이 아니라 rhs의 객체까지 적용된다.
해결법은 세 가지가 있다.
- operator=의 처음에 일치성 검사를 통해 자기대입을 점검한다.
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
하지만 완벽한 해결법은 아니다. new Bitmap을 수행할 때, 예외가 발생하면 Widget 객체는 결국 삭제된 Bitmap을 가리키는 포인터를 갖게 된다.
- 위의 문제를 예외에 안전하게 동작하도록 순서를 변경한다.
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // 원래의 pb를 기억
pb = new Bitmap(*rhs.pb); // pb가 *pb의 사본을 가리키게
delete pOrig; // 원래의 pb 삭제
return *this;
}
pb를 먼저 삭제하지 말고 이 포인터가 가리키는 객체를 복사한 후 삭제하면 된다.
new Bitmap 부분에서 예외가 발생하더라도 pb는 변경되지 않은 상태가 유지되기 때문이다.
일치성 검사가 없음에도 완벽하게 처리할 수 있다.
효율적은 측면에서 보면 자기대입이 일어나는 빈도가 적기 때문에 매번 일치성 검사를 함수의 초반에 진행하는 것보다 효율적일 수 있다.
- 복사 후 맞바꾸기
class Widget {
...
void swap(Widget& rhs); // *this의 데이터와 rhs swap
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // rhs의 사본 만들기
swap(temp); // *this의 데이터와 맞바꾸기
return *this;
}
이 방법은 C++이 가진 두 가지 특징을 활용한 것이다.
- 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다.
- 값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다.
Widget& Widget::operator=(Widget rhs) // rhs는 원래 객체의 사본
{
swap(rhs); // *this의 데이터를 사본과 바꿈
return *this;
}
객체를 복사하는 코드가 함수 본문으로부터 매개변수의 생성자로 옮겨졌기 때문에 컴파일러가 더 효율적으로 동작할 가능성이 있다.