자기 대입(self assignment)이란?
어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말합니다.
1 2 3 4 5 6 | class Widget { ... }; Widget w; ... w = w; // 자기에 대한 대입 |
위와 같은 구현을 하는 사람은 극히 드물겠지만, 예를 들어
a[i] = a[j]; 와 같이 i 및 j 가 같은 값을 갖게 되면 자기 대입문이 됩니다.
*px = *py; 와 같이 px 및 py가 가리키는 대상이 같으면 자기 대입문이 되고 맙니다.
위와 같이 이러한 자기대입이 생기는 이유는?
여러 곳에서 하나의 객체를 참조하는 상태, 다시말해 중복참조(aliasing)라고 불리는 것 때문입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Bitmap { ... }; class Widget { ... public: Widget& operator=(const Widget& rhs) { delete pb; // 현재 bitmap 삭제 pb = new Bitmap(*rhs.pb); return *this; } private: Bitmap* pb; }; void main() { Bitmap b; Widget w1, w2; w1.pb = w2.pb = &b; // w1, w2 같은 비트맵을 가르키고 있다. w1 = w2; } |
위의 구현의 문제점은?
같은 비트맵을 가르키게된 상태에서 대입 연산자가 호출되고, delete pb로 인해 두개의 w1, w2객체의 bitmap은 사라지게 됩니다. 그런데 이미 지워진 비트맵으로 pb를 생성하려 하니 예외가 발생하게 되는 것 입니다.
중요한것은
코드에서도 보면 아시겠지만, 하나의 객체를 여러곳에서 참조 하고 있는 상태에서 자기대입의 문제점을 말하고 있습니다.
위의 문제점은 객체가 같은지 즉 자기대입인지 검사문장(일치성테스트)으로 해결은 됩니다.
if( this == & rhs ) return *this;
말그대로, 자기대입이면, 아무것도 안하고 리턴하게끔 하는 것입니다.
하지만 위의 코드로는 자기대입에 대한 처리가 있을때의 예외처리만이 가능할뿐이지
if 문을 통과 하여 new 연산시 동적 할당에 필요한 메모리가 부족하다든지, 다양한 예외가 나오게 되면, Widget객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가지고 남습니다.
위와 같은 문제점을 해결하기 위해서는?
1 2 3 4 5 6 7 8 9 | Widget& Widget::operator=(const Widget& rhs){ // delete pb; pb를 바로 삭제하지 않고, 복사본을 만든다. Bitmap * pOri = pb; pb = new Bitmap(*rhs.pb); delete pOri; return *this; } |
위의 구현은 예외 안정성과, 자기대입 안정성을 동시에 가졌지만, 가장 효율적인 방법이라고는 할 수 없습니다.
사실상,
operator= 작성에 아주 자주 쓰이는 '복사 후 맞바꾸기(copy and swap) 기법을 들수 있습니다.
1 2 3 4 5 6 7 | Widget& Widget::operator=(const Widget& rhs){ Widget temp(rhs); // rhs의 데이터에 대해 사본을 하나 만듭니다. swap(temp); // *this의 데이터를 그 사본의 것과 맞바꿉니다. return *this; } |
위의 코드처럼 구현하게되면,
대입되는 객체가 자기대입인지는 상관없고, 무조건 사본을 만들어 사본과 자신을 바꾸기때문에 자기 자신이면 그냥 Swap을 해도 자기자신이 되는거고, 아니라면 값이 바뀌는 것입니다.
또한,
rhs의 복사본 temp를 지역으로 선언 되어있다. 그리고 swap함수를 통해 this와 temp를 데이터를 바꾸게 되는데, 바꾼후 temp에는 this의 Bitmap포인터가 있을것이고, this는 rhs의 포인터로 바껴있을것이다. 이렇게 되면 자동적으로 블럭이 끝나는 순간 temp는 소멸되면서 말그대로 this의 Bitmap포인터가 소멸되는 것이다. 바꿔주면서 삭제가 자동이루어진 코드인 것이다.
예제중에서는 예외의 안정성과, 자기대입 안정성을 두루 갖춘 구현이라고 할 수 있겠습니다.
이것 만은 잊지 말자!
◆ operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만듭시다. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정 할 수도 있으며, 복사후 맞바꾸기 기법을 써도 됩니다.
◆ 두개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게ㅔ 동작하는지 확인해 보세요.
'0x0001 > Effective C++' 카테고리의 다른 글
[Effective C++] 항목 13 : 자원 관리에는 객체가 그만! (0) | 2019.02.12 |
---|---|
[Effective C++] 항목 12 : 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2019.02.12 |
[Effective C++] 항목 10 : 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2019.02.11 |
[Effective C++] 항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2019.02.10 |
[Effective C++] 항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2019.02.10 |