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

자기대입(self-assignment)이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말한다. w = w; 이 코드는 적법한 코드로 동작에는 문제가 없다. 다만 코드에서 명확하지 않은 자기대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태, 중복참조(aliasing)가 발생하기 때문이다.

같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려해야 한다. 사실은 같은 클래스 계통의 객체라도 굳이 똑같은 타입으로 선언할 필요없이 파생 클래스의 타입의 객체를 참조하거나 가리키는 용도의 기본 클래스의 참조자나 포인터를 사용하면 된다.

이후에 나올 항목 13, 14의 조언을 봐도 자원 관리 용도로는 항상 객체를 만들어야 하고 해당 객체를 복사될 때 나름대로 잘 동작하도록 설계하고 코딩할 때도 생각을 잘 해야한다. 이 과정에서 조심해야 할 부분이 바로 대입 연산자이다. 즉, 대입 연산자에 대해 안전하게 동작하도록 해야한다.

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

    return *this;
}

겉보기에 멀쩡해 보이는 이 코드는 의미적으로는 문제가 없을 것 같지만 자기 참조의 가능성이 있는 위험천만한 코드이다. 문제는 operator=내부에서 *this와 rhs가 같은 객체일 가능성이 있다는 것이다. 이 둘이 같은 객체라면 delete연산자에서 자기 자신의 메모리를 삭제하려는 시도를 하게 되는 예기치 못한 결과가 발생한다.

이런 에러에 대한 대책은 operator=내부에서 일치성 검사를 통해 자기대입을 점검하는 것이다. if (this == &rhs) return *this;

즉, 대입 연산자에서 발생하는 삭제 내부의 비트맵을 지우고 할당하는 경우와 같은 사용자 정의 타입의 대입 연산자에서는 자기대입을 점검하는 것이 좋다.

operator=을 예외에 안전하게 구현하면 대개 자기대입에도 안전한 코드가 나오게 되어 있다. 예외 안전성에만 집중하면 자기대입에도 안전한 코드가 나오게 되어 있다. 즉, 예외 안전성에만 집중하면 자기대입 문제는 무시하더라도 무사히 넘어갈 확률이 높다.

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

    return *this;
}

이 코드는 예외에 안전하다. 문장 순서를 조금 바꾸게 되면 new Bitmap 부분에서 예외가 발생하더라도 pb는 변경되지 않은 상태가 유지된다. 조금 더 생각해볼 내용은 효율을 너무나 신경 쓴 나머지 일치성 테스트를 함수 앞에 다시 두고 싶을 수 있다. 실제 개발에서 자기대입이 얼마나 일어나는지 좀 더 생각해보는 것이 좋다.

일치성 검사는 공짜가 아니다. 일치성 검사 코드가 들어가면 그만큼 코드가 커지는데다가, 처리 흐름에 분기를 만들게 되므로 실행 속력이 확 떨어질 수 있다. CPU 명령어 선행인출, 캐시, 파이프라이닝 등의 효과가 떨어질 수 있다.

앞서 다룬 방법 말고도 예외 안정성과 자기대입 안정성을 동시에 가진 operator=을 구현하는 방법으로, ‘복사 후 맞바꾸기(copy and swap)’이 있다.

정리

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

자기대입의 경우 크게 문제가 되는지 생각하지 못했다가, c++특성상 포인터라는 특성으로 삭제나 의도치 않게 중복되는 경우가 종종 발생할 수 있다는 사실을 알게 된 것 같다.

아무래도 책에서 말하는대로 데이터영역에서 주로 발생할 문제로 보인다. 좀 더 구글링 해보니 이동 생성자와 이동 대입 연산자를 이용해 자기대입을 처리하는 방법도 있다는 것을 알게 되었다. 이 방법이나 스마터포인터를 사용하는게 가장 좋은 방법이라고 생각한다.

태그: ,

카테고리:

업데이트:

댓글남기기