Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

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

책/Effective C++

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

아롱로또 2023. 11. 8. 13:01

다루는 내용

자기대입에 대한 처리가 되어있지 않은 경우 어떤 문제가 발생하는지, 자기대입에 대해 어떻게 처리할 수 있는지 다룬다.

 

자기대입이란 (self - assignment)

어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말한다.

class Widget{ };

int main(){
    Widget w;
    ...
    w = w;
    ...
}

굉장히 어색하고 쓸 일이 없어보이는 코드지만 위 코드는 문제없이 컴파일이 성공하여 눈에 띄지 않을 것이다.

또한 다음처럼 생각보다 자주 발생할 수 있다.

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

위 코드에서 만약 i와 j가 동일한 값을 가질 때, 자기대입이 발생한다.

포인터 변수 px와 py가 동일한 값을 가리킬 때, 자기대입이 발생한다.

같은 타입으로 만들어진 객체 여러 개를 참조자나 포인터를 사용하여 동작하는 코드를 작성할 때, 중복참조(aliasing)가 발생할 수 있으므로 같은 객체가 사용될 가능성을 고려하자.

자기대입에 대한 처리가 없는 경우 문제점

class Bitmap{...};

class Widget{
private:
    Bitmap* p_bitmap; // Heap에 할당한 객체를 가리키는 포인터 변수
    ...
public:
    Widget& operator=(const Widget& rhs){
        delete p_bitmap;
        p_bitmap = new Bitmap(*rhs.p_bitmap);
        return *this;
    }
};

위 코드의 Widget& oprator= 함수에서, 자기대입이 일어난다고 가정해보자.

*this가 rhs 변수와 같다면 delete p_bitmap 이 *this 객체의 p_bitmap뿐만 아니라 rhs 객체의 p_bitmap에도 영향을 미칠 것이다. 해당 Widget 객체는 자기대입 실행으로 인해 자신이 포인터 멤버 변수를 통해 갖고 있던 객체가 삭제될 것이다.

 

자기대입 처리하기

1. 일치성 검사(Identity Test)

Widget& operator=(const Widget& rhs){
    if(*this == rhs) return *this;

    delete p_bitmap;
    p_bitmap = new Bitmap(*rhs.p_bitmap);

    return *this;
}

위와 같이 대입 연산자에서 rhs와 *this가 동일한지 확인해주면, 자기대입에 대한 문제를 해결할 수 있다.

이러한 방식은 자기대입에 대한 문제는 해결되었지만 예외 안전성에 대한 문제가 발생할 가능성이 있다.

new 연산자를 통해 Bitmap 객체를 새로 할당하면서 동적 할당에 필요한 메모리가 부족하거나 Bitmap 클래스 복사 생성자에서 예외가 발생한다면, new 이전에 delete를 먼저 진행하므로 p_bitmap은 delete된 채로 남게 될 것이다.

2. 복사 후 삭제

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

    return *this;
}

p_bitmap을 delete하기 전에, 임시 포인터 변수를 하나 만들어 저장해놓고, new 연산이 문제없이 실행될 경우 delete 하는 코드이다. 위 코드는 일치성 검사에 대한 코드가 없어도 자기대입에 대해서 올바르게 작동한다.

 

불안한 마음에 이전 코드처럼 일치성 검사 코드를 추가할 수도 있겠지만, 일치성 검사 코드를 추가하면 그만큼 소스 코드와 목적 코드가 커지는데다가 처리 흐름에 분기를 만들게 되므로 실행 시간 속력이 감소할 수 있다. 또한 CPU 명령어 선행인출, 캐시, 파이프라이닝 등의 효과도 감소할 수 있다.

 

3. 복사 후 맞바꾸기(Copy and Swap)

void swap(Widget& rhs){
    // rhs의 데이터와 *this의 데이터를 맞바꾸는 함수.
}
Widget& operator=(const Widget& rhs){ // Call by Reference
    Widget temp(rhs);
    swap(temp);
    return *this;
}

이후에 다룰 예외 안전성과 밀접한 관계에 있는 방법이다. 이러한 방법은 operator= 작성에 굉장히 자주 쓰이므로 알아두자.

 

C++의 특징을 이용해 다음처럼도 작성 가능하다.

Widget& operator=(Widget rhs){ // Call by Value
    swap(rhs);
    return *this;
}

값에 의한 전달로 parameter를 받으므로 별도의 객체를 생성하지 않고 바로 swap 함수를 호출하는 것이다.

객체를 복사하는 코드가 함수 본문으로부터 매개변수의 생성자로 옮겨졌으므로 컴파일러가 더 효율적인 코드를 생성할 수 있는 여지가 만들어진다.

이것만은 잊지 말자!

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