11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자
a[i] = a[j]; // 자기대입 가능성이 크다
*px = *py; // 자기대입 가능성이 크다
위의 코드에서 i와 j가 같은 값이라면 자기대입, px와 py가 가리키는 대상이 같다면 자기대입이 된다. 이러한 자기대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태, 다시 말해 중복참조(aliasing)라고 불리는 것 때문이다. 대입 연산자는 우리가 신경쓰지 않아도 자기대입에 대해 안전하게 동작해야 한다.
동적 할당된 비트맵을 가리키는 원시 포인터를 데이터 멤버로 갖는 클래스를 하나 만들었다고 가정해 보자.
class Bitmap{};
class Widget
{
public:
Widget& operator=(const Widget& rhs) // 안전하지 않은 operator=
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
private:
Bitmap* pb; // 힙에 할당한 객체를 가리키는 포인터
};
문제가 없을 것 같지만 자기 참조의 가능성이 있는 위험천만의 코드다. operator= 내부에서 *this(대입되는 대상)와 rhs가 같은 객체일 가능성이 있다. 이 둘이 같은 객체라면 *this객체의 비트맵과 rhs의 객체까지 delete처리가 되버린다. 결국 자신의 포인터 멤버를 통해 물고 있던 객체가 삭제된 상태가 되어 버린다.
일치성 검사
이런 에러에 대한 대책 중 전통적인 방법은 일치성 검사(identity test)를 통해 자기대입을 점검하는 것이다.
Widget& operator=(const Widget& rhs)
{
if (this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
하지만 new Bitmap 부분에서 예외가 터지게 되면(동적 할당에 필요한 메모리가 부족하다든지 Bitmap 클래스 복사 생성자에서 예외를 던진다든지 해서), Widget 객체는 결국 삭제된 Bitmap을 가리키는 포인터를 껴안고 홀로 남게 된다.
문장 순서 바꾸기
문장 순서를 바꾸는 것으로 예외에 안전한 코드를 만들 수 있다. pb를 무턱대고 삭제하지 말고 이 포인터가 가리키는 객체를 복사한 직후에 삭제하면 해결된다.
Widget& operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
new Bitmap 부분에서 예외가 발생하더라도 pb(그리고 이 포인터가 들어 있는 Widget)는 변경되지 않은 상태가 유지된다. 게다가 일치성 검사 같은 것이 없음에도 불구하고 이 코드는 자기대입 현상을 완벽히 처리하고 있다. 원본 비트맵을 복사해 놓고, 복사해 놓은 사본을 포인터가 가리키게 만든 후, 원본을 삭제하는 순서로 실행되기 때문이다.
복사 후 맞바꾸기 (copy and swap)
또 다른 방법으로 복사 후 맞바꾸기 기법이 있다.
class Widget
{
public:
void swap(Widget& rhs); // *this의 데이터 및 rhs의 데이터를 맞바꾼다.
Widget& operator=(const Widget& rhs)
{
Widget temp(rhs); // rhs의 데이터에 대해 사본을 하나 만든다.
swap(temp); // *this의 데이터를 사본의 것과 맞바꾼다.
return *this;
}
private:
Bitmap* pb; // 힙에 할당한 객체를 가리키는 포인터
};
이 방법은 C++가 가진 두 가지 특징을 활용하여 조금 다르게 구현할 수도 있다.
- 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다.
- 값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다는 점
Widget& operator=(Widget rhs)
{
swap(rhs);
return *this;
}
이것만은 잊지 말자!
- operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들자. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 된다.
- 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인해 보자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 13: 자원 관리에는 객체가 그만! (0) | 2023.10.09 |
---|---|
[Effective C++] 12: 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2023.10.08 |
[Effective C++] 10: 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2023.10.06 |
[Effective C++] 09: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (1) | 2023.10.05 |
[Effective C++] 08: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (1) | 2023.10.05 |