ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5. C++11 객체 생성 규칙 Rule of Five
    Modern C++/Move Semantics 2020. 7. 1. 23:55

    C++11 이전 컴파일러가 생성하는 특수 멤버 함수

    C++11 이전 컴파일러는 기본 생성자(Basic Constructor), 소멸자(Destructor) , 복사 생성자(Copy Constructor), 복사 할당자(Copy Assignment)를 필요한 경우 자동으로 생성한다. 

    이는 컴파일 도중 해당 연산을 사용하는 코드가 있는 경우 컴파일러에 의해 암묵적으로 public inline으로 자동으로 생성 된다. 

    class Foo {
    };
    
    Foo f0;  //기본 생성자 생성
    Foo f1 = f0; //복사 생성자 생성
    Foo f1(f0); //Foo f1 = f0 와 동일 
    Foo f2; 
    Foo f2 = f0; // 할당 연산자 생성 
    

    위의 Foo 클래스는 사용자가 아무것도 정의하지 않았지만 컴파일러가 Foo 객체 사용 여부를 보고 아래와 동일하게 생성한다. 

    class Foo {
    public:
    	inline Foo() {};
        inline ~Foo() {};
        inline Foo(const Foo& rhs) {};
        inline Foo& operator = (const Foo& rhs) { return *this; };
    };
    

    클래스에 다른 생성자가 정의되어 있는 경우 기본 생성자는 만들어 지지 않는다. 

    class Bar {
    public:
        Bar(int a): a(a) {};
    private:
        int a;
    };
    
    Bar b0; //기본 생성자가 없으므로 컴파일되지 않음 
    

    상속 시 소멸자의 경우 부모 클래스가 가상(virtual)일 경우 자식 클래스도 가상(virtual)으로 자동 생성된다. 

    class Base {
    public:
    	virtual ~Base() {}
    }
    
    class Derived : public Base {
    public:
    	//가상 소멸자를 갖는 부모 클래스를 상속받는 경우 아래와 같이 가상으로 자동 생성
    	//inline virtual ~Derived();
    }; 
    
    

    Rule of Three

    C_++11 이전에 클래스 설계 시 소멸자(Destructor), 복사 생성자(Copy Constructor), 복사 할당자(Copy Assignment) 셋 중 하나라도 사용자가 직접 구현한다면 나머지 둘도 모두 구현해야 한다. 

     

    해당 클래스가 자원을 직접 관리하는 경우 소멸자에서는 해당 자원을 해제하게 되는데 복사 생성자로 생성된 클래스는 복사 원본과 동일한 자원을 가리키게 되고 원본 클래스와 복사 클래스가 소멸자를 호출 시 동일한 자원에 대한 해제가 두 번 발생하게 된다. 이를 아래 Data 클래스를 통해서 살펴보자. 

    class Data{
    public:
        Data();
        Data(const uint8_t* data, size_t length);
        virtual ~Data();
        const uint8_t* data() const { return data_; }
        uint8_t* data() { return data_; }
        size_t length() { return length_; }
        uint8_t* data_;
        size_t length_;
    };
    
    Data::Data() :data_(NULL), length_(0) {}
    
    Data::Data(const uint8_t *data, size_t length) {
        data_ = new uint8_t [length]; //data_ 배열의 메모리 생성
        length_ = (data_ != NULL) ? length : 0;
        std::memcpy(data_, data, length_);
    }
    
    Data::~Data() {
        if(data_) {
            delete [] data_; //data_ 배열의 메모리 삭제 
            data_ = NULL;
            length_ = 0;
        }
    }
    
    int main() {
        Data d(reinterpret_cast<const uint8_t*>("abcd"), 4);
        std::cout << d.data() << std::endl;
        {
            Data copy_d = d; //해당 객체가 블록 범위를 벋어나면 data_ 배열이 소멸됨.
        }
        return 0;
    }

    실행 결과

    copy_d 는 복사 생성자를 통해 생성되는 데 이때 copy_d.data_ =  d.data_; 와 같이 컴파일러에 의해 할당되는 데 이는 원본의 data_ 배열을 가리키게 된다. 이 후 copy_d는 블록 범위를 벗어나게 되고 소멸자가 호출되어 d_data_ 배열을 삭제하게 된다. 이후 d 객체가 메인 함수 범위를 벗어 날 때 data_ 배열에 다시 한 번 소멸자가 호출되 이미 삭제된 메모리를 다시 삭제하게 되어 예외가 발생하고 프로세스가 종료하게 된다. 이와같이 클래스 자원을 관리하고 소멸자에서 해당 자원을 반환하는 경우에는 소멸자를 직접 작성하게 되는 데 이때 복사 생성자와 복사 연산자를 같이 작성해 주어야 한다. 

    class Data{
    public:
    	...
        Data(const Data& rhs);
        Data& operator = (const Data& rhs);
        ...
    };
    
    ...
    Data::Data(const Data& rhs) {
        data_ = new uint8_t [rhs.length_];
        length_ = (data_ != NULL)? rhs.length_ : 0;
        std::memcpy(data_, rhs.data_, length_);
    
    }
    
    Data& Data::operator = (const Data& rhs) {
        if(this == &rhs) { return *this; }
        if(data_ != NULL) { delete [] this; }
    
        data_ = new uint8_t [rhs.length_];
        length_ = (data_ != NULL)? rhs.length_ : 0;
        std::memcpy(data_, rhs.data_, length_);
        return *this;
    }
    ...

    C++11 부터 컴파일러가 생성하는 특수 멤버 함수

    C++11 에는 컴파일러가 생성하는 특수 멤버 함수에 이동 생성자(Move Constructor), 이동 연산자(Move Assignment)가 추가되어 총 5가지 의 특수 멤버 함수를 해당 함수를 사용할 때 컴파일러가 자동으로 만들어 낸다. 

    class Foo {
    };
    
    Foo f0;  //기본 생성자 생성
    Foo f1 = f0; //복사 생성자 생성
    Foo f1(f0); //Foo f1 = f0 와 동일 
    Foo f2; 
    Foo f2 = f0; // 할당 연산자 생성 
    
    Foo f3 = std::move(f0); // 이동 생성자 생성
    Foo f4;
    f4 = std::move(f1); //이동 할당 연산자 생성 

    컴파일러는 아래와 동일한 클래스를 생성한다. 

    class Foo {
    public:
    	inline Foo() {};
        inline ~Foo() {};
        inline Foo(const Foo& rhs) {};
        inline Foo& operator = (const Foo& rhs) { return *this; };
        inline Foo(Foo&& rhs) {};
        inline Foo& operator = (Foo&& rhs) {};
    };
    
    

    Rule of Five

    Rule of Three 와 동일하게 C++11에는 클래스 설계 시 소멸자(Destructor), 복사 생성자(Copy Constructor), 복사 할당자(Copy Assignment), 이동 생성자(Move Constructor), 이동 할당자(Move Assignment) 다섯 중 하나라도 사용자가 직접 구현한다면 나머지 넷도 모두 구현해야 한다.

     

    Data 클래스에 복사 생성자, 복사 할당자, 이동 생성자와 이동 연산자를 정의하지 않는 경우 컴파일러는 암묵적으로 이를 생성한다. 

    복사 생성자와 복사 연산자의 경우 컴파일러는 rhs의 멤버 변수들을 해당 클래스의 멤버 변수에 각각 할당하게 된다.

    Data::Data(const Data& rhs)
    :data_(rhs.data_), length_(rhs.length_) {
    }
    
    Data& Data::operator = (const Data& rhs) {
        data_ = rhs.data_;
        length_ = rhs.length_;
        return *this;
    }

    따라서 복사 생성자나 복사 할당연산자를 직접 구현하지 않으면 문제가 발생함을 위해서 확인 했다. 동일하게 이동 생성자와 이동 할당자를 구현하지 않으면 컴파일러는 아래와 같은 코드를 생성한다.

    Data::Data(Data&& rhs) 
    : data_(std::move(rhs.data_)), length_(std::move(rhs.length_)) {
    }
    
    Data& Data::operator = (Data &&rhs) {
        data_ = std::move(rhs.data_);
        length_ = std::move(rhs.length_);
        return *this;
    }

    멤버 변수 uint8_t * 는 이동 시 아무일도 일어나지 않으므로 소스클래스와 이를 통해 이동 생성되거나 이동 할당된 클래스는 동일한 data_를 가리키고 두 클래스가 소멸 시 모두 data_ 를 삭제하고자 해 문제가 발생한다. 이를 위해서는 4개의 모든 함수를 사용자가 작성해 주어야한다. 

    class Data{
    public:
        Data();
        Data(const uint8_t* data, size_t length);
        virtual ~Data();
        const uint8_t* data() const { return data_; }
        uint8_t* data() { return data_; }
        size_t length() { return length_; }
    
        Data(const Data& rhs);
        Data& operator = (const Data& rhs);
    
        Data(Data&& rhs);
        Data& operator = (Data&& rhs);
    
        bool vaild() {
            return data_ != nullptr;
        }
    
    private:
        uint8_t* data_;
        size_t length_;
    };
    
    Data::Data() :data_(nullptr), length_(0) {}
    
    Data::Data(const uint8_t *data, size_t length) {
        data_ = new uint8_t [length];
        length_ = (data_ != nullptr) ? length : 0;
        std::memcpy(data_, data, length_);
    }
    
    Data::~Data() {
        if(data_) {
            delete [] data_;
            data_ = nullptr;
            length_ = 0;
        }
    }
    
    
    Data::Data(const Data& rhs) {
        data_ = new uint8_t [rhs.length_];
        length_ = (data_ != NULL)? rhs.length_ : 0;
        std::memcpy(data_, rhs.data_, length_);
    
    }
    
    Data& Data::operator = (const Data& rhs) {
        if(this == &rhs) { return *this; }
        if(data_ != nullptr) { delete [] this; }
    
        data_ = new uint8_t [rhs.length_];
        length_ = (data_ != nullptr)? rhs.length_ : 0;
        std::memcpy(data_, rhs.data_, length_);
        return *this;
    }
    
    Data::Data(Data&& rhs) {
        data_ = std::exchange(rhs.data_, nullptr);
        length_  = std::exchange(rhs.length_, 0);
    }
    
    Data& Data::operator = (Data&& rhs) {
        data_ = std::exchange(rhs.data_, nullptr);
        length_  = std::exchange(rhs.length_, 0);
        return *this;
    }

    클래스 정의 시 복사 생성자, 복사 할당자를 직접 정의하는 경우 이동 생성자, 이동 할당 연산자는 컴파일러가 이를 암묵적으로 생성하지 않는다. 따라서 해당 클래스에 이동 생성자와 이동 할당자를 직접 정의하지 않으면 이동 불가능한 클래스가 된다. 

    이동 불가능한 객체를 std::move를 통해서 이동 생성하거나 이동 할당 하는 경우 해당 하는 복사 생성자 와 복사 할당자가 이를 대체하는 데 아래 링크를 통해 확인할 수 있다. 

     

    2020/06/30 - [Modern C++] - 4. 이동가능 객체(Movable Type)

     

    4. 이동가능 객체(Movable Type)

    클래스를 이동 가능하게 만들면 많은 장점들이 있고 이동 가능한 사용자 정의 타입은 표준 라이브러리 유틸과도 잘 동작한다. 이동 연산을 지원하는 클래스를 만드는 경우 코드의 성능, 안정성�

    object-world.tistory.com

    이동 생성자와 이동 할당자 만 사용자가 정의하는 경우 복사 생성자와 복사 할당자는 암묵적으로 컴파일러가 생성하지 않는다. 이를 통해 이동만 가능한 객체를 만들 수 있는 데, std::thread, std::unique_ptr, std::unique_lock 등이 대표적인 예이다. 

    struct MoveOnlyClass {
        MoveOnlyClass(){};
        MoveOnlyClass(MoveOnlyClass&& rhs) {}
        MoveOnlyClass& operator = (MoveOnlyClass&& rhs) {}
    };
    
    MoveOnlyClass m0;
    MoveOnlyClass m1 = m0; //복사 생성자는 암묵적으로 생성되지 않기 때문에 컴파일 되지 않음
    auto m2 = std::move(m0); //이동 생성만 가능 

    'Modern C++ > Move Semantics' 카테고리의 다른 글

    4. 이동가능 객체(Movable Type)  (0) 2020.06.30
    3. 완벽 전달(Perfect Forwarding)  (1) 2020.06.29
    2. std::move  (0) 2020.06.29
    1. 이동 의미론(Move Semantics)  (1) 2020.06.29

    댓글

Designed by Tistory.