자기대입(self assignment) |
- #include <iostream>
- using namespace std;
- class Widget{};
- void main()
- {
- Widget w;
- w = w; //자기대입
- }
문제는 무엇인가? |
같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적으로 바람직한 자세라고 할 수 있겠습니다. 사실, 같은 클래스 계통에서 만들어진 객체라 해도 굳이 똑같은 타입으로 선언할 필요까지는 없습니다. 파생 클래스 타입의 객체를 참조 하거나 가리키는 용도로 기본 클래스의 참조자나 포인터를 사용하면 되니까 말이죠. 그럼 대입연산자에서는 무엇을 조심해야 할까요? 아래와 같이 동적 할당된 비트맵을 가리키는 원시 포인터를 데이터 멤버로 갖는 클래스가 있다고 한번 해봅시다.
- #include <iostream>
- using namespace std;
- class Bitmap
- {
- public:
- Bitmap()
- {
- cout << "Bitmap ()" << endl;
- }
- };
- class Widget
- {
- public:
- Widget()
- {
- cout << "Widget ()" << endl;
- pb = new Bitmap;
- pb = NULL;
- }
- Widget (const Bitmap& pbitmap)
- {
- cout << "Widget (const char * data)" << endl;
- pb = new Bitmap(pbitmap);
- memcpy(pb, &pbitmap, sizeof(pbitmap));
- }
- Widget& operator=(const Widget& rhs) // 안전하지 않게 구현된 operator=
- {
- cout << "Widget& operator=(const Widget& rhs)" << endl;
- delete pb;
- pb = new Bitmap(*rhs.pb);
- return *this;
- }
- private:
- Bitmap * pb; //힙에 할당된 객체를 가르키는 포인터
- };
- int main()
- {
- Bitmap b;
- Widget w(b);
- Widget w2; // 디폴트 생성자에서 꼭 초기화를 해줘야 한다.
- w2 = w; // 자기 대입 검사를 하지 않으면, 심각한 오류가 발생한다.
- return 0;
- }
물론 이것에 대한 대책은 있습니다.
- Widget& operator=(const Widget& rhs)
- {
- cout << "Widget& operator=(const Widget& rhs)" << endl;
- if (this == &rhs) // 일치성 검사, 즉 객체가 같은지, 자기 대입인지 검사한다.
- return *this;
- delete pb;
- pb = new Bitmap(*rhs.pb);
- return *this;
- }
전부는 아니지만, operator=을 예외에 안전하게 구현하면 대개 자기대입에도 안전한 코드가 나오게 되어 있습니다. 즉, 예외 안전성에만 집중하면 자기대입 문제에 대해서는 그렇게 걱정을 안해도 된다는 의미죠. 여기에서는 pb를 무조건 삭제 하지 말고 이 포인터가 가리키는 객체를 복사한 직후에 삭제하면 예외에 대한 문제를 해결 할 수 있습니다. 아래와 같이 말이죠.
- Widget& operator=(const Widget& rhs) // 이 코드는 예외에 안전하다.
- {
- cout << "Widget& operator=(const Widget& rhs)" << endl;
- Bitmap * pOrig = pb; // 원래의 pb를 pOrig에 저장해둔다.
- pb = new Bitmap(*rhs.pb); // 다음, pb가 *pb의 사본을 가리키게 만든다.
- delete pOrig; // 원래의 pb를 삭제한다.
- return *this;
- }
위의 코드는 new Bitmap 부분에서 예외가 발생하더라도 pb는 변경되지 않는 상태가 유지되기 때문에 예외에 안전합니다. 또한 원본 비트맵을 복사해 놓고, 복사해 놓은 사본을 포인터가 가리키게 만든 후, 원복을 삭제하는 순서로 실행되기 때문에 일치성 검사 없이도 자기대입 현상을 완벽히 처리 하고 있죠. 물론 이 방법 말고도 다른 복사 후 맞바꾸기(Copy and Swap) 이라는 방법도 있습니다.
복사 후 맞바꾸기(Copy and Swap) |
- #include <iostream>
- using namespace std;
- class Bitmap
- {
- public:
- Bitmap()
- {
- cout << "Bitmap ()" << endl;
- }
- };
- class Widget
- {
- public:
- Widget()
- {
- cout << "Widget ()" << endl;
- pb = new Bitmap;
- pb = NULL;
- }
- Widget (const Bitmap& pbitmap)
- {
- cout << "Widget (const char * data)" << endl;
- pb = new Bitmap(pbitmap);
- memcpy(pb, &pbitmap, sizeof(pbitmap));
- }
- void swap(Widget& rhs) // *this의 데이터 및 rhs의 데이터를 맞바꾼다.
- {
- Bitmap * pOrig = pb;
- pb = new Bitmap(*rhs.pb);
- rhs.pb = pOrig;
- }
- Widget& operator=(const Widget& rhs)
- {
- cout << "Widget& operator=(const Widget& rhs)" << endl;
- Widget temp(rhs); // rhs의 데이터에 대해 사본을 하나 만든다.
- swap(temp); // *this의 데이터를 그 사본의 것과 맞바꾼다.
- delete temp.pb;
- return *this;
- }
- private:
- Bitmap * pb; // 힙에 할당된 객체를 가르키는 포인터
- };
- int main()
- {
- Bitmap b;
- Widget w(b);
- Widget w2; // 디폴트 생성자에서 꼭 초기화를 해줘야 한다.
- w2 = w; // 자기 대입 검사를 하지 않으면, 심각한 오류가 발생한다.
- return 0;
- }
- 첫째, 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다.
- 둘째, 값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다
- //rhs는 넘어온 원래 객체의 사본 -- '값에 의한 전달'
- Widget& operator=(Widget rhs)
- {
- cout << "Widget& operator=(const Widget& rhs)" << endl;
- swap(rhs); // *this의 데이터를 그 사본의 데이터와 맞바꾼다.
- delete rhs.pb;
- return *this;
- }
* 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인하자.
댓글 없음:
댓글 쓰기