티스토리 뷰

C++

[C++] Rule of three/five/zero

nodeal 2019.01.05 14:32

C++을 사용하면서 자료를 다루는 class는 수도 없이 많이 사용될 것이다.


하지만 그 자료가 완전히 메모리에 안전한 인스턴스일 수도 있고, raw pointer일 수도 있을 것이다.


이 때에 맞추어 각 class를 구성하는 방법에 대하여 알아보자.




Rule of zero :

소멸자, 복사/이동 생성자, 복사/이동 할당연산자가 정의되어있는 class의 wrapper class는 다음의 꼴을 가진다.


class Test {
    private:
    std::string s;
    
    public:
    Test(const std::string &s) : s(s) {}
};


소멸자, 복사, 이동 생성자, 복사, 이동 할당연산자가 정의되어있는 class는 OOP 단일 책임 원칙에 따라 새로운 소멸자, 생성자, 할당연산자가 정의되어선 안된다.




Rule of three :

Raw pointer를 가지고 있는 wrapper class는 적당한 할당과 해제가 정의되어있는 소멸자, 복사 생성자, 복사 할당연산자를 정의해야 한다.


class Test {
    private:
    char* s;
    
    public:
    Test() : s(new char[0]) {}
    
    Test(const char* s, std::size_t size) : s(new char[size + 1]) {
        std::copy(s, s + size + 1, this->s);
    }
    
    Test(const Test& test) : Test(test.s, std::strlen(test.s)) {}
    
    ~Test() {
        delete[] s;
    }
    
    Test& operator=(const Test& test) {
        delete[] s;
        auto size = std::strlen(test.s);
        s = new char[size + 1];
        std::copy(test.s, test.s + size, s);
        return *this;
    }
};

Rule of zero에서와 같이 raw pointer를 제어하는 기능을 포함하지 않는다면(default를 사용한다면) Test class는 복사할 때 raw pointer에 대한 얕은 복사만 이루어지게 된다.


하지만 이런 방법으로는 C++11에서 도입된 이동 연산을 가리게 된다.




Rule of five :

굳이 모든 자료가 깊은 복사가 이루어질 필요없이 일시적으로 생성되는 객체의 자료를 이동하는 방법을 사용할 수도 있어야할 것이다.


다음의 경우에서,

auto s = "Hello World!";
Test test;
test = Test(s, std::strlen(s));

Rule of three만 적용되어있는 Test class는 test를 생성하는 과정에서 불필요한 복사를 야기시킨다.


이에 따라 rule of three에서 가리게 되었던 이동 생성자, 할당 연산자를 다시 정의할 필요가 있다.


class Test {
    private:
    char* s;
    
    public:
    Test() : s(new char[0]) {}
    
    Test(const char* s, std::size_t size) : s(new char[size + 1]) {
        std::copy(s, s + size + 1, this->s);
    }
    
    Test(const Test& test) : Test(test.s, std::strlen(test.s)) {}
    
    Test(Test&& test) noexcept : s(std::exchange(test.s, nullptr)) {}
    
    ~Test() { delete[] s; }
    
    Test& operator=(const Test& test) {
        delete[] s;
        auto size = std::strlen(test.s);
        s = new char[size + 1];
        std::copy(test.s, test.s + size, s);
        return *this;
    }
    
    Test& operator=(Test&& test) {
        std::swap(s, test.s);
        return *this;
    }
};

이때 다시 위의 예제를 실행한다면, 이동 할당연산자가 호출되어 바로 사라질 객체인 Test(s, std::strlen(s))으로부터의 복사를 방지할 수 있다.




정리하자면, raw pointer를 제어하는 class의 경우 각각에 맞는 할당과 해제를 맡을 복사 생성자, 소멸자, 할당연산자를 정의해야하고


C++11 이상에서 연산의 손실을 방지하기 위해서는 이동 생성자와 할당연산자 또한 직접 정의해야한다.



'C++' 카테고리의 다른 글

[C++] 안전한 포인터 설계  (0) 2019.01.26
[C++] Rule of three/five/zero  (0) 2019.01.05
댓글
댓글쓰기 폼