센로그
[EC++] 11. operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 본문
자기대입이란?
- 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 의미한다.
class Widget {...};
widget w;
...
w = w; // 자기대입
- 얼핏 보면 딱히 사용될 일이 없어보인다. 그러나, 다음과 같은 상황은 어떨까?
a[i] = a[j];
- 위 구문은 i == j일 때, 자기대입문이 된다.
*px = *py;
- 위 구문은 px와 py가 가리키는 대상이 같을 때, 자기대입문이 된다.
자기대입이 생기는 이유는?
- 중복참조(aliasing) 시에 자기 대입이 발생할 가능성이 크다.
- 여러 곳에서 하나의 객체를 참조하는 상태
- 같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는, 같은 객체가 사용될 가능성을 고려하는 것이 바람직하다.
자기대입 시 생길수 있는 문제
class Bitmap { ... };
class Widget {
...
private:
Bitmap* pb; // 동적 할당한 객체를 가리키는 포인터
};
- Widget 클래스의 멤버 중, 동적 할당한 비트맵 객체를 가리키는 포인터가 있다고 하자.
Widget& Widget::operator= (const Widget& rhs) {
delete pb; // 현재 비트맵 삭제
pb = new Bitmap(*rhs.pb); // rhs의 비트맵을 사용하기로 함
return *this;
}
- 위와 같이 대입 연산자를 구성하면 좌변 객체의 비트맵을 우변 객체의 비트맵으로 바꿔줄 수 있다.
- 그러나, 만약 두 객체가 같은 객체였다면?
- 삭제된 비트맵 객체를 참조하게 된다...!
해결 방안 1. 일치성 검사
- 가장 간단한 해결책으로, 대입 연산자 첫머리에서 두 객체가 같은지 검사하면 된다.
Widget& Widget::operator= (const Widget& rhs) {
if (this == &rhs) return *this; // 자기대입인지 검사
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
- 그러나 여전히 문제가 있는데, 예외에 안전하지 않다는 것이다.
- 만약, delete pb;를 마치고 pb에 새로운 비트맵을 할당해주려고 하는데, 이 new 과정에서 메모리 부족 등의 예외가 터지게 된다면?
- 결국 좌변 객체는 삭제된 비트맵을 가리키는 포인터를 그대로 갖게 된다...!
해결 방안 2. 순서를 잘 바꿔보자
- 원래 pb를 기억해두고 삭제할 새 포인터를 만든다.
- pb가 rhs의 비트맵을 가리키게 만든다.
- 아까 만든 새 포인터로 원래 pb를 삭제한다.
Widget& Widget::operator= (const Widget& rhs) {
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
};
- 이 코드는 예외에 안전하며, 자기대입에도 안전하다.
- new 부분에서 예외가 터지면 delete 구문이 실행되지 않으므로, pb는 그대로 원본 비트맵을 가리키는 상태이다.
- 자기 대입문일지라도, pb가 rhs의 비트맵의 사본을 만들어 가리키는 시점에 rhs의 비트맵이 삭제되었을 가능성이 없으므로 안전하다.
- 이 코드는 제법 비효율적으로 보일 수 있다.
- 자기대입이 그렇게 자주 일어나는 것도 아닌데, 할 때마다 저런 과정을 거쳐야 하나!!! 생각할 수도 있다.
- 그래서 걍 일치성 검사를 하고싶어질 수도 있다.
- 그런데 일치성 검사 하는 것도 공짜는 아니다.
- 일치성 검사를 하면 분기를 만들게 되므로 실행 시간 속력이 줄어들 수도 있다.
해결 방안 3. copy and swap
- 해결 방안 2와 비슷한 다른 방안이 하나 더 있다.
class Widget {
...
void swap(Widget& rhs);
};
Widget& Widget::operator= (const Widget& rhs) {
Widget temp(rhs);
swap(temp);
return *this;
}
- 문장 실행 순서를 의도적으로 조작하는 대신, 사본 객체 하나를 만들어서 둘을 바꿔주는 것이다. (복사 생성자 사용)
- 이를 pass by value로 구현할 수도 있는데, 다음과 같다.
Widget& Widget::operator= (Widget rhs) {
swap(rhs);
return *this;
}
- 이 경우 자동으로 사본으로 넘어오므로 복사 생성자를 사용하지 않아도 된다.
- 객체를 복사하는 코드가 함수 본문으로부터 매개변수의 생성자로 옮겨졌기 때문에, 컴파일러가 더 효율적인 코드를 생성할 수 있는 여지가 생긴다고 한다.
- 이 대목에서 어떤 최적화가 일어날 수 있는 건지는 잘 모르겠다.
복사 생성자 호출 과정에서 임시 객체가 생기나 했는데 그건 또 아닌 것 같다. 흠...
- 이 대목에서 어떤 최적화가 일어날 수 있는 건지는 잘 모르겠다.
- 그러나 아무래도 객체를 값으로 전달하는 것은 좀..그렇고, 코드의 명확성이 좀 떨어지긴 한다.
'Effective > Effective C++' 카테고리의 다른 글
[EC++] 13. 자원 관리에는 객체가 그만! (0) | 2024.11.18 |
---|---|
[EC++] 12. 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2024.11.18 |
[EC++] 10. 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2024.11.17 |
[EC++] 9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2024.11.17 |
[EC++] 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2024.11.16 |
Comments