Rule of Zero

728x90

많은 내용을 다루다 보니 제목이 다소 이상하게 지어졌다. 본 포스트에서 다룰 내용은 아래와 같다. 

  • Constructor (생성자)
  • Destructor (소멸자)
  • Copy / Move Constructor (복사 / 이동 생성자)
  • Copy / Move Assignment (복사 / 이동 대입 연산자)

 

통상적으로 클래스를 만들 때 생성자를 만들어 주는 경우가 많다 나머지 4개의 메소드는 컴파일러가 자동으로 만들어 주기 때문에 생략하고 클래스를 구성해도 무방하다. 하지만, 리소스를 포인터로 관리하는 경우(Raw pointer)에는 컴파일러가 자동으로 만들어주는 메소드로는 사용이 안될 경우에 위 5개의 메소드를 다 직접 정의해주어야 한다. 

 

컴파일러가 만들어주는 메소드를 사용하면 안 되는 이유는 간단하다. 기본 Destructor (소멸자)를 사용하게 될 경우에는 포인터가 가리키고 있는 동적 할당된 메모리가 해제가 안되기 때문이다. 또한 Copy Construtor와 Copy Assignment가 동적 할당된 메모리 자체를 복사하는 것 (Deep copy)이 아닌 "Shallow copy"만 일어나기 때문이다. 

 

클래스를 구성할 때 아래 세 가지 룰을 따른다. 

 

Rule of Zero

클래스의 Instance로 Raw pointer가 존재하지 않게 클래스를 구성하는 방법이다. Raw pointer가 존재하지 않다면 따로 수동적으로 메모리 할당을 해제해줄 필요가 없기 때문에 5개의 멤버 함수를 다 정의해줄 필요가 없다. 

class rule_of_zero
{
    std::string cppstring;
 public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {}
};

 

Rule of Three

클래스의 Instance로 Raw pointer가 존재할 경우 위에서 말한 "Shallow copy"문제와 메로리 할당을 컴파일러가 자동으로 정의해주는 소멸자에서 해제가 안되기 때문에 Destructor, Copy Constructor 그리고 Copy Assignment를 정의해주어야 한다. 

#include <cstddef>
#include <cstring>
 
class rule_of_three
{
    char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
    rule_of_three(const char* s, std::size_t n) // to avoid counting twice
    : cstring(new char[n]) // allocate
    {
        std::memcpy(cstring, s, n); // populate
    }
 public:
    rule_of_three(const char* s = "")
    : rule_of_three(s, std::strlen(s) + 1)
    {}
    ~rule_of_three() // I. destructor
    {
        delete[] cstring;  // deallocate
    }
    rule_of_three(const rule_of_three& other) // II. copy constructor
    : rule_of_three(other.cstring)
    {}
    rule_of_three& operator=(const rule_of_three& other) // III. copy assignment
    {
        if (this == &other) return *this;
        std::size_t n{std::strlen(other.cstring) + 1};
        char* new_cstring = new char[n]; // allocate
        std::memcpy(new_cstring, other.cstring, n); // populate
        delete[] cstring;  // deallocate
        cstring = new_cstring;
        return *this;
    }
 public:
    operator const char *() const { return cstring; } // accessor
};

 

 

Rule of Five

Rule of Three에서 모든 Copy가 Deep Copy가 될 필요가 없는 경우가 생길 필요가 없기 때문에 데이터의 소유권을 가져올 수 있는 Move Constructor와 Move Assignment까지 정의를 해야 한다. 

class rule_of_five
{
    char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
 public:
    rule_of_five(const char* s = "")
    : cstring(nullptr)
    { 
        if (s) {
            std::size_t n = std::strlen(s) + 1;
            cstring = new char[n];      // allocate
            std::memcpy(cstring, s, n); // populate 
        } 
    }
 
    ~rule_of_five()
    {
        delete[] cstring;  // deallocate
    }
 
    rule_of_five(const rule_of_five& other) // copy constructor
    : rule_of_five(other.cstring)
    {}
 
    rule_of_five(rule_of_five&& other) noexcept // move constructor
    : cstring(std::exchange(other.cstring, nullptr))
    {}
 
    rule_of_five& operator=(const rule_of_five& other) // copy assignment
    {
         return *this = rule_of_five(other);
    }
 
    rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment
    {
        std::swap(cstring, other.cstring);
        return *this;
    }
 
// alternatively, replace both assignment operators with 
//  rule_of_five& operator=(rule_of_five other) noexcept
//  {
//      std::swap(cstring, other.cstring);
//      return *this;
//  }
};

위 코드들은 cpprefernce에서 가져온 코드이다. 더 자세한 내용은 아래 링크를 참조하길 바란다. 

https://en.cppreference.com/w/cpp/language/rule_of_three

 

The rule of three/five/zero - cppreference.com

[edit] Rule of three If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three. Because C++ copies and copy-assigns objects of user-defined types in va

en.cppreference.com

 

 

아래는 필자가 Raw pointer 없이 Rule of Five를 따르는 클래스이다. 

/*
한개의 메소드만 가지고 있지만 컴파일러가 알아서 메소드들을 만들어 준다 CGF - Compiler Generaterd Functions
1. constructor
2. destructor
3. copy / move constructor
4. copy / move assignment
일반적으로 생성자는 우리가 만들어 주는 경우가 많지만
아래 5개의 메소드는 일반적으로 우리가 만들지 않는다. 
*/
#include <iostream>
#include <string>

class Dog
{
public:
	Dog() = default;		//default constructor 다른 constructor를 만들고 기본 생성자를 통해 생성할 경우 오류가 남. 컴파일러가 만든것을 사용한다.
	Dog(std::string name, int age) : mName{ name }, mAge{ age } {};		//constructor

	~Dog() {};	//destructor

	Dog(const Dog& other) : mName{ other.mName }, mAge{ other.mAge }	//copy constructor
	{
		//std::memcpy();	포인터가 존재할 경우 메모리 복사
	}	

	Dog(Dog&& other) noexcept : mName{ std::move(other.mName) }, mAge{ other.mAge }	//move constructor
	{
		//mptr = other.mPtr		other이 가르키고있는 주수를 복사하고 ohter의 포인터를 nullptr로 초기화 시켜준다 (소유권 이전)
		//other.mPtr = nullptr;
	}

	Dog& operator=(const Dog& other)	//copy assignment
	{
		if (&other == this)		//포인터를 이용한 리소스일 경우를 대비해서 만약 같은 객체를 copy하려가 한다면 자신의 객체를 바로 반환한다. 
		{
			return *this;
		}
		mName = other.mName;		//대입 연산자로 other의 이름과 나이를 copy함
		mAge = other.mAge;

		return *this;
	}

	Dog& operator=(Dog&& other) noexcept	//move assignment
	{
		if (&other == this)
		{
			return *this;
		}
		mName = std::move(other.mName);		//대입 연산자로 other의 이름의 소유권을 가져옴
		mAge = other.mAge;	//대입 연산자로 ohter의 나이를 단순히 복사

		return *this;
	}

	void print()
	{
		std::cout << mName << " " << mAge << std::endl;
	}
private:
	std::string mName;
	int mAge;
};

int main()
{
	Dog choco{ "choco", 9 };
	Dog mong{ "mong", 8 };
	
	choco = choco;
	choco = std::move(choco);		//포인터를 이용한 리소스일 경우  문제가 생길 수 있다. 

	mong = std::move(choco);
	mong.print();
	choco.print();
}

 

 

Ref.

https://en.cppreference.com/w/cpp/language/rule_of_three

https://www.youtube.com/watch?v=aSrFrUOeQP0&list=PLDV-cCQnUlIYTBn70Bd822n2nlJwafCQE&index=7 

+ Recent posts