[Effective C++ 3판] Chapter 2. 생성자, 소멸자 및 대입 연산자 (항목 5~12)

Chapter 2 . 생성자, 소멸자 및 대입 연산자

항목 5 : C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자.
항목 6 : 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자.

이러한 것과 관련된 규칙으로서 rule of three/five/zero 라는 것이 있다. 아래 링크 참조.

[ 정리해 두고 싶은 것 & 추가적인 사항 ]
// C++11에서는 =delete를 사용하면 되긴 하지만, 책을 읽다가 인상깊어서 남겨본다.
// 부스트에도 비슷하게 noncopyable이라는게 있다고 한다.
class UnCopyable {
protected:
    UnCopyable() {}
    ~UnCopyable() {}

private:
    // 선언만 함으로서 friend에 대해서도 안전성 확보.
    UnCopyable(const UnCopyable&);
    UnCopyable& operator=(const UnCopyable&);
};
class Example : private UnCopyable {
};

[ 이것만은 잊지 말자! ]
- 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있습니다. (C++11에서는 이동 생성자, 이동 대입 연산자가 암시적으로 생성될 수 있다.
- 컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후에 구현은 하지 않은 채로 두십시오. Upcopyable과 비슷한 기본 클래스를 쓰는 것도 한 방법입니다. (C++11에서는 =delete 를 활용하자.)

항목 7 : 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자.


가상함수를 하나라도 가진 경우 소멸자를 가상 소멸자로 선언하는 방법이 괜찮은 것 같다.

기본 클래스로 설계되지 않아서 상속을 막고 싶은 경우에는 C++11에서는 final을 사용하고, C++0x 이전에서는 꼼수를 사용한다.


[ 정리해 두고 싶은 것 & 추가적인 사항 ]
// 추상 클래스로 쓰고 싶은데 마땅한 순수 가상함수가 없을 때,
// 소멸자를 순수 가상함수로 선언하면 된다.
class AMOV {  // Abstract w/o Virtuals
public:
    virtual ~AMOV() = 0;
};
AMOV::~AMOV() {}  // 정의하지 않으면 링크에러.

class Example : public AMOV {
};

[ 이것만은 잊지 말자! ]
- 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다.
- 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다. (C++11에서는 기본 클래스로 설계되지 않은 경우에 final keyword를 이용해 상속을 막도록 하자!)

항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자.

소멸자에서는 예외를 빠져나가게 해선 안된다. 만약 stack unwinding 수행 중에, 소멸자에서 또 예외를 던지면 노답상황이 된다. 이럴경우 프로그램은 종료되게 된다.
// Case 1의 경우에는 정상적으로 작동한다.
// 그러나, Case 2의 경우에는 스택풀기중에 또 예외가 발생하기 때문에 노답상황이 된다.
class Example {
public:
    ~Example() {
        throw 1;
    }
};

int main() {
    try {
        Example ex;  // case 1) ok.
    }
    catch (...) {
        // Can reach here.
    }
    try {
        Example exs[2];  // case 2) not ok.
    }
    catch (...) {
        // Program aborts before it reaches here.
    }
    return 0;
}

[ 이것만은 잊지 말자! ]
- 소멸자에서는 예외가 빠져나가면 안 됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 합니다. (C++11부터는 소멸자가 암시적으로 noexcept로 선언되어 예외를 던질시 런타임에 std::terminate가 호출된다.)
- 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(소멸자가 아니라)이어야 합니다.

항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자.

Base클래스와 그를 상속받는 Derived클래스가 있을 때 Derived객체가 생성될 때, Base생성자 내부에서는 객체의 타입은 Base이다. 즉, 이 때 가상 함수를 호출 하게 되면 Base의 함수가 호출될 것이다. 따라서 객체 생성 및 소멸 과정에서는 가상 함수를 호출하면 안된다. 만약 이런 경우가 연출된다면 클래스 구조를 바꿔야한다. 

[ 이것만은 잊지 말자! ]
- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않으니까요.

항목 10 : 대입 연산자는 *this의 참조자를 반환하게 하자.
항목 11 : operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자.

[ 정리해 두고 싶은 것 & 추가적인 사항 ]
// Full Source : https://gist.github.com/taeguk/dfc4790cac1c92de6ecfc6bd93ebe70f

// Self assignment : Unsafe
// Exception : Unsafe
class Unsafe {
public:
    Data *pData;
    Unsafe& operator=(const Unsafe& obj) {
        delete pData;
        pData = new Data(*obj.pData);
        return *this;
    }
};

// Self assignment : Safe
// Exception : Unsafe
class Unsafe2 {
public:
    Data *pData;
    Unsafe2& operator=(const Unsafe2& obj) {
        if (this == &obj)
            return *this;
        delete pData;
        pData = new Data(*obj.pData);  // if exception occurs in new??
        return *this;
    }
};

// Self assignment : Safe
// Exception : Safe
class Safe {
public:
    Data *pData;
    Safe& operator=(const Safe& obj) {
        Data *pOrg = pData;
        pData = new Data(*obj.pData);
        delete pOrg;
        return *this;
    }
};

// Self assignment : Safe
// Exception : Unsafe
class Unsafe3 {
public:
    Data *pData, *pData2;
    Unsafe3& operator=(const Unsafe3& obj) {
        Data *pOrg = pData, *pOrg2 = pData2;
        pData = new Data(*obj.pData);
        pData2 = new Data(*obj.pData2);  // if exception occurs in this new??
        delete pOrg;
        delete pOrg2;
        return *this;
    }
};

// Using "copy and swap".
// Self assignment : Safe
// Exception : Safe
class RealSafe {
public:
    Data *pData, *pData2, data3;
    void swap(RealSafe &&obj) noexcept {
        using std::swap;
        swap(pData, obj.pData);
        swap(pData2, obj.pData2);
        data3 = std::move(obj.data3);
    }
    RealSafe& operator=(const RealSafe& obj) {
        RealSafe copy(obj);
        // std::swap(*this, copy);  // BAD!! Infinite loop occurs!
        swap(std::move(copy));
        return *this;
    }
};

[ 이것만은 잊지 말자! ]
- 대입 연산자는 *this의 참조자를 반환하도록 만드세요.
- operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만듭시다. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 됩니다.
- 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인해 보세요.

항목 12 : 객체의 모든 부분을 빠짐없이 복사하자.

자기 자신의 멤버 변수들을 초기화하는 것은 기본이고 상속한 기본 클래스의 복사 생성자(복사 대입 연산자)를 꼭 호출해주어야한다.
복사 생성자와 복사 대입 연산자는 비슷한 기능을 수행하게 될 텐데, 코드 중복을 피하기 위해 한쪽에서 다른 쪽을 호출하도록 하는 행위는 절대로 안된다. 개념상으로도 복사 생성자는 '초기화', 복사 대입 연산자는 '대입'의 기능을 수행하기 때문이고, 실제로도 복사 생성자는 초기화리스트를 활용하기 때문에 한쪽에서 다른 쪽을 호출하는 건 안된다. 단, 코드 본문에서 겹치는 부분이 많다면 따로 제 3의 함수로 빼서 그것을 호출하는 식으로 구현하는 것이 코드중복 해결책일 것이다.

[ 이것만은 잊지 말자! ]
- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 합니다.
- 클래스의 복사 함수 두 개(복사 생성자, 복사 대입 연산자)를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 마세요. 그 대신, 공통된 동작을 제 3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결합시다.



이로서 두 번째 chapter에 대한 포스팅도 마치도록 하겠습니다~ 
5/8일에 정보처리산업기사 시험이 있어서 공부를 좀 해야할 것 같아서 다음 포스팅은 5/8일 이후가 될 것 같습니다 ㅎㅎ
아 근데 글 얼마 되지도 않는 데 포스팅하는데 왜케 시간이 오래걸리는지;; 
다음부터는 좀 스피디하게 포스팅해야겠습니다. (책 읽는 시간보다 포스팅 시간이 훨씬 오래걸리는 기이한 현상,,,;; ㅠㅠㅜㅠ 아무래도 코드를 직접 다시짜고 실험해보는 시간때문인 것 같네요.)
다음 포스팅에서 만나요~~

댓글