본문 바로가기
Working on

[Effective C++] operator= 에서는 자기대입에 대한 처리가 빠지지 않게하자

by Warehaus 2021. 5. 19.

Self assignment는 어느 객체가 자신에 대입 연산자를 적용하는 것을 말한다.

class Item {};

Item i;
...

i = i; // Self assignment

문장 자체는 적법한 문장이다. 모두가 의미 없는 코드임을 알 것이다. 아래와 같은 코드도 자기 대입이 가능한 코드들이다.

a[i] = a[j];

*px = *py;

이런 자기대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 중복 참조(Aliasing) 때문이다.

같은 타입을 다루는 객체들을 참조자나 포인터로 동작하도록 코드로 작성하는 경우 이런 현상이 나올 수 있음을 고려하는 것이 필요하다.

같은 클래스 계통에서 만들어진 객체가 반드시 똑같은 타입으로 선언 될 필요는 없으며, 파생 클래스 타입의 객체를 참조하거나 가리키는 용도로 기본 클래스의 참조자나 포인터를 사용해도 된다.

operator= 구현 시 자기참조의 가능성이 있는 코드의 경우 위험할 수 있는데, 아래 예시를 통해 확인해 보자.

class Color {};

class Apple {
private:
    Color *c;
};

Apple&
Apple::operator=(const Apple& a) {

    delete c;
    c = new Color( *a.c );
    
    return *this;
}

위 예시에서 문제는 operator= 내부에서 *this와 a 가 같은 객체일 가능성이 있다는 점이다.  왜냐면, 이 함수가 끝날 때 해당 Apple 객체는 포인터 멤버를 통해 객체가 삭제될 수도 있기 때문이다.

전통적으로는 operator=의 첫 머리에서 일치성 검사(Identity test)를 통해 자기 대입을 점검하는 방법이다.

Apple& Apple::operator=(const Apple& a)
{
    if ( this == &a ) return *this;
    
    delete c;
    c = new Color(*a.b);
    
    return * this;
}

이렇게 구현 하더라도, 'new Color' 표현식에서 예외가 발생하면, 결국에는 Apple에서는 삭제된 Color를 갖게 되는 것이다.  이런 일이 발생하게 되는 경우 지워진 Color 포인터를 처리하기가 상당히 까다로우며, 상당한 시간을 투자하여 디버깅을 해야 하는 경우가 발생할 수 있다.

이런 문제를 개선하기 위해 아래와 같이 추가적으로 수정해 보자.

Apple& Apple::operator=(const Apple& a)
{
    Color *cOrigin = c;
    c = new Color(*a.c);
    delete cOrigin;
    
    return * this;
}

위 코드는 new Color 부분에서 예외가 발생하더라도, c는 변경되지 않는 상태를 유지한다. 그러므로 delete 된 포인터에 대한 방어가 가능하며, 일치성 검사 같은 것이 없더라도 자기대입 현상을 완벽히 처리가 가능하다.

이 외에도 복사 후 맞바꾸기(Copy and Swap) 기능이 있는데, 이는 다음 포스팅에서 다뤄보도록 한다.