[Effective C++] 항목 11 : operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자

수민이슈·2023년 3월 19일
0

Effective C++

목록 보기
11/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들자!
1) 원본 객체와 복사 대상 객체의 주소 비교
2) 문장의 순서를 적절히 조정
3) 복사 후 맞바꾸기 기법
💡 두 개 이상의 객체에 대해 동작하는 함수가 있다면,
함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확히 동작하는지 확인해봐야 한다!


🖊️ 자기대입?

자기대입 (self assignment)

: 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것

class Widget { ... };
Widget w;
...
w = w; 						// 자기대입
a[i] = a[j];				// if (i == j) 자기대입
*px = *py; 					// if (px가 가리키는 대상 == py가 가리키는 대상) 자기대입

자기대입이 생기는 이유

중복참조 (alising)

: 여러 곳에서 하나의 객체를 참조하는 상태

같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때,
같은 객체가 사용될 가능성을 고려해야 한다.

파생 클래스 타입의 객체를 참조하거나 가리킬 때 기본 클래스의 참조자, 포인터를 사용해도 되니까
굳이 같은 타입으로 선언할 필요는 없당


🖊️ 자기대입은 위험하다

자원을 사용하기 전에 해제할 수도 있다.

예시 상황

class Bitmap { ... };
class Widget {
	...
private:
	Bitmap* pb;			// 힙에 할당한 객체를 가리키는 포인터
};

Widget& Widget::operator= (const Widget& rhs) {
	delete pb;							// 현재 비트맵 삭제
    pb = new Bitmap(*rhs.pb);			// rhs의 비트맵을 사용하자
    return *this;
}

마 장난하나

문제점이 많음

*this와 rhs가 같은 객체일 경우

delete 연산자가 *this.pb에만 적용되는게 아니라, *rhs.pb에도 적용된다.
그니까 Widget 객체가 가리키는 객체가 삭제되어버린다.

해결 방법 1 : 일치성 검사 (identity test)

Widget& Widget::operator= (const Widget& rhs) {
	if (this == &rhs) return *this;				// 자기대입인지 검사. 자기대입이면 걍 자기자신 리턴
    
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

이 경우 new Bitmap() 부분에서 예외가 터지면,
delete pb; 까지만 적용되고 끝난다
그러면 결국 pb 포인터는 또 삭제된 Bitmap을 가리키게 된다.

해결 방법 2 : 순서를 바꿔보자

Widget& Widget::operator= (const Widget& rhs) {
	Bitmap* pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
};

1) 원본 비트맵 복사
2) 복사해 놓은 사본을 포인터가 가리키도록 함
3) 원본 삭제

이렇게 되면 아까처럼 new Bitmap()에서 예외가 발생하더라도 pb는 변경되지 않은 상태가 유지된다!
(삭제된 객체를 가리킬 확률이 없다)

근데,, 자기대입은 자주 일어나진 않는다.
일치성 검사를 코드에 집어넣으면 분기가 생기는거라서 성능이 뚝뚝 떨어진다.

해결 방법 3: 복사 후 맞바꾸기 (copy and swap)

class Widget {
	...
    void swap(Widget& rhs);			// *this의 데이터, rhs의 데이터 swap
};

Widget& Widget::operator= (const Widget& rhs) {
	Widget temp(rhs);
    swap(temp);
    return *this;
}

rhs에 대한 사본 temp를 복사 생성자를 통해 만들어버림
그리고 temp와 *this를 swap.
rhs가 자기 자신이면 결국 자기 자신을 리턴할거고 아니면 바꿔주겠지

temp는 operator=의 지역에 선언되어 있음
*temp.pb*this.pb가 있을 때
두 개를 swap해버리면
temp에는 this의 pb가,
this에는 temp의 pb가 있을거다

*this를 리턴하고 함수를 빠져나오면
자연스럽게 temp는 소멸되고 그러면서 this의 pb도 같이 삭제될거다
개꿀


그리고

c++은
1) 클래스의 복사 대입 연산자인자를 값으로 취하도록 선언하는게 가능하다.
2) 값에 의한 전달 (Call-By-Value)은 전달된 대상의 사본이 생긴다.

Widget& Widget::operator= (Widget rhs) {
	swap(rhs);
    return *this;
}

따라서 이렇게 할 수 있따

여기서 call by value
인자로 들어온 rhs는 rhs의 사본임
그래서 rhs의 사본과 *this를 바꿔버린다


😊 느낀점

와우
그냥 자기대입 관련 부분이라고 생각했는데,
코드를 뜯어보고 설명하려고 한줄한줄 쳐다보니까 느낀게 많다
지역으로 선언해놔서 자동으로 소멸시킨 부분은 걍 소름이 돋는다
내가 멍충이라 그렇겠지만 아무래도 ..
괜찮아 배우면 되는거지!

0개의 댓글