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

 

1
2
3
4
5
6
7
8
9
    Widget a;
 
    a = a;            //대놓고 미친짓.
 
    int arr1[1000];
 
    arr1[i] = arr[i]; //복잡한 반복문에서 발생할 수도 있는 일.
 
    *px = *py;        //px, py가 가리키는 대상이 같으면 자기 대입이 된다.
cs

 

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

같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는

같은 객체가 사용될 가능성을 고려하는 것이 좋다.

 

특히 자원 관리와 엮여있는 경우에는 더욱 더 주의해야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Bitmap { ... };
 
class Widget
{
public:
    ...
    
    Widget& operator=(const Widget& rhs)
    {
        delete pb;
        pb = new Bitmap(rhs.pb);
        return *this;
    }
 
private:
    Bitmap* pb;
};
cs

 

상기와 같이 작성된 코드에서 자기 대입이 진행된다면, 좌변 객체(this)와 우변 객체(rhs)가 동일하므로 멤버 변수인 pb 또한

this의 것이기도 하면서 rhs의 것이기도 하다.

사용자의 의도는 원래 pb를 제거하고 다시 새로운 정보를 가지고 pb를 생성하려고 했던 것인데...

bp를 제거함으로써 this의 pb 뿐만 아니라 초기화 재료로 사용하려던 rhs의 pb 또한 제거가 된다.

 

그러므로 결국 operator= 함수에서 새로 생성되는 pb는 쓰레기 값을 가진 Data가 된다. (아니면 null이 되어 null exception 발생)

 

해결 방법 1

 

전통적인 방법으로 operator=의 첫머리에 일치성 검사를 통해 자기대입을 검사한다.

 

1
2
3
4
5
6
7
8
    Widget& operator=(const Widget& rhs)
    {
        if (this == &rhs) return this//자기 대입이라면 아무것도 하지 않는다.
 
        delete pb;
        pb = new Bitmap(rhs.pb)
        return *this;
    }
cs

 

그러나 이 방법은 예외에는 안전하지 않다.

무슨 말이냐하면, new Bitmap 부분에서 예외가 터지게 되면 (동적 할당에 필요한 메모리 부족, 복사 생성자에서

예외 발생 등...), Widget 객체는 삭제된 Bitmap을 가리키는 포인터를 껴안고 남게된다.

즉, 아무것도 가리키지 않는 포인터가 하나 남게되고 이것을 delete 시켜줄 방법 또한 없다.

 

또한 if문으로 조건 처리하는데 속도 저하에도 영향을 미칠 수 있다.

 

해결 방법 2.

 

1
2
3
4
5
6
7
8
    Widget& operator=(const Widget& rhs)
    {
        Bitmap* pTemp = pb;
        pb = new Bitmap(rhs.pb);
        delete pTemp;
 
        return *this;
    }
cs

 

이 방법은 자기대입과 예외에 모두 처리가 된 코드이다. (문장 순서를 바꾸는 것만으로 예외에 안전한 코드가 만들어진다)

 

1. 원본 비트맵을 복사해놓고 (pTmpe와 pb가 가리키고 있는 비트맵을 동일하다.)

2. rhs의 pb를 가지고 새로운 Bitmap을 생성하여 가리키게 한다.

   (즉, pb는 신규 Data를 가리키고 pTemp는 이전 Data를 가리키고 있게된다.)

   (자기 대입일지 언정 같은 내용으로 새로 만든 객체를 가리키는 것이니 문제 없다.)

3. 그 후 pTemp를 delete 해줌으로써 이전 pb Data를 안전하게 제거한다.

4. 또한 new Bitmap에서 예외가 발생한다고 하더라도 pb는 이전 Data를 그대로 가리키고 있기 때문에 아무 문제가 없다.

   그리고 pTemp 변수 자체는 스택에 할당되기 때문에 예외 발생 시 operator= 함수가 종료되면 pTemp는 자동적으로 소멸된다.

 

해결 방법 3.

 

다른 방법도 있다. 복사 후 맞바꾸기(copy and swap)이라고 알려진 기법인데, 이 기법은 예외 안정성과 매우 밀접한 관계를

가지고 있어 자세한 내용은 뒤에서 다루지만 이 기법은 operator= 함수에 아주 자주 쓰이는 기법이므로 간략히 설명한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget
{
    void swap(Widget& rhs); //*this의 데이터 및 rhs의 데이터를 swap
    ...
};
 
Widget& Widget::operator=(const Widget& rhs)
{
    Widget temp(rhs); //rhs의 데이터 사본을 하나 만들고
 
    swap(temp);       //*this(현재 객체)의 데이터를 사본과 맞바꾼다.
 
    return *this;
}
cs

 

c++이 가진 특성을 활성해서 다르게도 구현할 수 있다.

 1. 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다는 점.

 2. 값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다는 점.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget
{
    void swap(Widget& rhs); //*this의 데이터 및 rhs의 데이터를 swap
    ...
};
 
Widget& Widget::operator=(Widget rhs) //rhs는 넘어온 원래 객체의 사본
{                                      //Call by value
 
    swap(rhs);    //*this(현재 객체)의 데이터를 사본 데이터와 맞바꾼다.
 
    return *this;
}
cs

 

값에 의한 전달은 조금 걱정되는 코드이지만 객체를 복사하는 코드를 복사 생성 연산자 본문에서 하지 않고

매개변수의 생성자로 옮겨졌기 때문에, 컴파일러가 더 효율적인 코드를 생성할 수 있는 여지가 만들어진다.

 

2줄 요약

 

 - operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들자.

   원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 조정해도 되며, 복사 후 맞바꾸리를 해도 된다.

 

 - 두개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게

   동작하는지 확인해 보자.

+ Recent posts