상세 컨텐츠

본문 제목

항목 11) operator= 에서 자기 대입 상황에 대한 처리를 반드시 하자

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 13:59

본문

operator= 에서 자기대입에 대한 처리가 빠지지 않도록 하자

 

자기 대입 ( self assignment ) 이란?

class Widget {...}
 
 
Widget w;
...
w = w;

위와 같이 자기 자신에게 대입 연산자를 적용하는 경우를 말한다.

따로 처리하지 않는다면, 컴파일러는 아무런 문제도 제기하지 않기 때문에 누구라도 작성이 가능하다. ( 설계자, 사용자 모두 )

단순히 위의 코드만 본다면 누가 저런식으로 사용할까 싶겠지만,

조금만 모습을 바꾸면 누구라도 실수할 수 있다.

a[i] = a[j];
...
*px = *py;

두 문장 모두 자기 대입 가능성을 가지고 있다.

물론 그러한 경우가 없도록 완벽하게 사용한다면 문제가 없겠지만,

설령 실수를 한다고 해도 컴파일러가 제지하지 않는다는 것이 문제인 것이다.

아래는 여러 문제점을 가지고 있는 대입 연산자 오버로딩 예시

class Bitmap { … }; // 동적으로 할당될 Bitmap
 
class Widget {
	...
private:
	Bitmap *pb; // 힙에 할당된 객체를 가리키는 포인터
};
 
//불안정한 '=' 연산자 구현코드
Widget& Widget::operator=(const Widget& rhs)
{
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}

대입 연산자 내부를 주목하자.

rhs 객체와 this( 자기자신 ) 객체가 같은 객체일 가능성이 존재한다.

그렇게 되면 처음에 삭제한 pb 포인터가 rhs 객체의 pb이기도 하기 때문에 해당 함수를 통해서 대입이 안 되는 것은 물론이고 원본 값이 삭제되는 것이다.

// 문제점 개선 코드 1)
class Bitmap { … }; // 동적으로 할당될 Bitmap
 
class Widget {
	...
private:
	Bitmap *pb; // 힙에 할당된 객체를 가리키는 포인터
};
 
//불안정한 '=' 연산자 구현코드
Widget& Widget::operator=(const Widget& rhs)
{
	if(this == &rhs) return *this;	// 자기 대입인지를 검사
 
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}

위처럼 자기 대입을 검사하는 코드를 추가하면 안전해진다.

더보기

테스트 코드의 비용에 대해서

 

그런데 이것이 정말 최선일까?

당연한 말이겠지만, 일치성 테스트 행위 역시 공짜가 아니다.

물론 한 두개 넣는다고 성능에 큰 문제를 주는 것도 아니지만,

명확하게 코드가 늘어나서 소스 및 목적 코드 둘 다 커지고, 처리 흐름에 분기가 더 생기기 때문에 실행 시간 역시 비교적 줄어든다.

( CPU 명령어 선행인출, 캐시, 파이프라이닝에도 영향을 줄 수 있다. )

또 다른 방법 중 하나가 'Copy and Swap' 기법인데, 이것은 예외 안정성과 관련이 깊으며 항목 29) 에서 자세히 다룬다.

그런데 위 코든에서 new Bitmap 부분에서 예외가 발생하면 해당 Widget 객체는 삭제된 pb를 가리키는 포인터를 가지게 된다.

당연히 이러한 상황은 바람직하지 않으므로 개선이 필요하다.

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

pb의 원본값을 어딘가에 ( pOrg) 저장해두고, *rhs.pb 로 생성된 new Bitmap을 pb가 가리키게 한다.

그런 뒤에 원본 pOrg를 삭제하고 *this를 반환하는 방식이다.

이렇게 되면 new Bitmap에서 예외가 발생하더라도 pb 자체는 안전하다. ( pOrg가 삭제되므로 )

또한 자기 대입에 현상도 처리하는데, 원본을 복사하고 복사한 사본을 포인터가 가리키게 만든 뒤에 원본을 삭제하는 순서이기 때문이다.

(즉, 어차피 사본에 대해서 연산이 이루어지기 때문에 자기 자신을 대입시켜 삭제하는 경우도 보완이 된다. )

물론 해당 방식이 가장 효율적이진 않지만, 동작에는 문제가 없다.

( copy and swap 기법이 왜 일치성 검사 코드보다 좋은 효율인지는 이해하지 못 함... )

 

Things to Must Remember

  • 대입 연산자를 오버로딩 할 때는 반드시 자기 자신이 대입 되는 경우에 대해서 처리해야한다. 일치성 검사, 문장 순서 변경, Copy and Swap 기법 등이 존재한다.
  • 두 개 이상의 객체를 파라미터로 받아서 동작하는 함수가 있다면, 두 객체가 동일한 객체인 경우에도 문제 없이 동작하도록 설계할 것.

관련글 더보기

댓글 영역