Effective C++/Chapter 2: 생성자, 소멸자 및 대입 연산자

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

yeoul0714 2025. 4. 23. 05:28

1. 개요


자기대입은 어떤 객체가 자기 자신에게 대입 연산을 실행하는 것을 말합니다.

 

예를 들면 이런것이죠

 

Yeoul y;

 

y=y;

 

문법상 아무 문제가 없는 코드입니다.

 

a[i] = a[j];

 

i와j가 같아지면 자기대입이 됩니다.

 

*pa = *pb;

 

둘이 가리키는 대상이 같으면 자기대입이 됩니다.

 

이러한 경우가 생기는 이유는 하나의 객체를 여러곳에서 참조하는 중복참조 상태 때문입니다.

 

이럴땐 같은 객체가 사용될 가능성을 고려하는것이 바람직한 자세입니다.

 

void DoSomething(const Base& rb, Derived* pd);

// 사실 rb와 *pb는 같은 객체였을지도 모릅니다.

 

결론은 자기 대입 연산은 생각보다 빈번하게 발생할 수 있다는 것이지요


 

2. 발생하는 문제들/해결법


 

2-1. 문제1

자기대입 연산자는 항상 안전하게 동작해야만 합니다.

 

그러나 아무런 고려없이 자기대입 연산자를 만들게 된다면 큰 문제에 봉착하게 될지도 모릅니다.

 

예시를 보도록 합시다.

class Animal {};
class Yeoul
{
private:
    Animal *pb;
};

Yeoul&Yeoul::operator=(const Yeoul& rhs)
{
    delete pb;
    pb = new Animal(*rhs.pb);
    
    return *this
}

 

코드를 얼핏보면 아무런 문제가 없어보일지도 모릅니다.

 

기존의 pb를 지우고 새롭게 대입된 rhs를 pb가 참조하게 하고 있으니까요

 

그러나 이 코드엔 심각한 문제가 한가지 있습니다.

 

operator= 내부에서 *this와 rhs가 같은 객체일 가능성이 있다는 것입니다. 

 

즉 자기대입을 실행할지도 모른다는 말이죠.

 

그렇다면 이것이 왜 문제가 될까요?

 

둘이 같은 객체라면 delete가 pb를 지워버리면 사실 rhs의 pb도 함께 지워지는 것입니다.

 

그렇게되면 결국 Yeoul객체는 자신이 들고있던 pb가 삭제되어 버리는 불상사가 발생합니다.

 

2-2. 문제1 해결책

해결책은 의외로 간단합니다.

 

그냥 같은 객체인지 확인해주면 됩니다.

Yeoul&Yeoul::operator=(const Yeoul& rhs)
{
    if(this == &rhs) return *this; // 만약 자기자신이 대입됐다면 아무것도 안합니다.
 
    delete pb;
    pb = new Animal(*rhs.pb);
    
    return *this
}

이렇게 해주면 같은 경우일땐 아무것도 안하기 때문에 문제가 해결됩니다.

 

2-3. 문제2

그러나 아직 이 해결책도 문제를 품고 있습니다.

 

그것은 바로 예외에는 안전하지 않다는 문제 때문입니다.

 

아직 이 코드는 예외가 터질경우에 대한 대비가 되어 있지 않습니다.

 

new Animal에서 예외가 터지게 된다면(동적할당 메모리 부족, 클래스 복사생성자에서 예외가 생길 때)

 

Yeoul객체는 삭제된 Animal pointer를 가지고 있게 됩니다.

 

2-4. 문제2 해결

신기하게도 문제2, 예외에 대해서 안전하게 구현된다면 문제1, 자기대입에 대한 문제도 보통 해결이 됩니다.

 

코드의 문장 순서를 조금만 세심하게 바꿔준다면 예외와 자기대입에 안전한 코드가 만들어지게 됩니다.

 

방법은 pb를 무작정 삭제하는 것이 아니라 이 포인터가 가리키는 객체를 복사하고 삭제하면 해결됩니다

 

 

Yeoul&Yeoul::operator=(const Yeoul& rhs)
{
    Animal *pOrigin = pb;
    pb = new Animal(*rhs.pb);
    delete pOrig;
    
    return *this;
}

만약 new Animal에서 예외가 터지더라도 Yeoul객체와 pb는 안전합니다.

 

또한 일치성을 검사하지 않아도 자기대입 문제도 해결하고 있습니다.

 

일치성 검사를 매번 실행하는 것도 상당한 CPU자원 낭비가 됩니다.

 

2-4. 문제2 해결법2

마지막 방법은 복사 후 맞바꾸기(copy and swap)이라는 방법입니다.

 

class Yeoul
{
    void Swap(Yeoul& rhs);
};

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

 

이 과정은 rhs 데이터에 대해서 사본 객체를 하나 만들고 

 

그 사본 객체와 *this의 데이터를 맞바꾸는 것입니다.

 

아래처럼 구현도 가능합니다.

Yeoul& Yeoul::operator=(Widget rhs)
{

    Swap(rhs);
    
    return *this;
}

 

1, 복사 대입 연산자는 인자를 값으로 취하도록 선언이 가능하다.

 

2. 값에 의한 전달을 하면 사본 객체가 생긴다.

 

이 두 조건을 이요한 방법입니다.

 

이것만은 잊지 말자!

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

 

https://yeoul0714.tistory.com/43

 

[Effective C++] 항목 12: 객체의 모든 부분을 빠짐없이 복사하자

1. 개요설계가 잘 된 클래스를 보면 객체를 복사하는 함수는 딱 2개 있습니다. 1. 복사 생성자2. 복사 대입 연산자 우리는 이 둘을 통틀어서 복사 함수라고 부릅니다. 우리는 컴파일러가 자동으로

yeoul0714.tistory.com