Rule of Zero
[C++] Copy / Move Constructor & Assignment (복사, 이동 생성자와 대입 연산자)
많은 내용을 다루다 보니 제목이 다소 이상하게 지어졌다. 본 포스트에서 다룰 내용은 아래와 같다.
- 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
아래는 필자가 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
'Modern C++' 카테고리의 다른 글
[C++] Class Keywords (클래스 키워드) : const, explicit (0) | 2021.08.19 |
---|---|
[C C++] Overloading (오버 로딩) (0) | 2021.08.19 |
[C++] Member Initializer List (멤버 초기화 리스트) (0) | 2021.08.18 |
[C C++] Static Member Function & Static Member Variable (정적 멤버 함수 및 변수) (0) | 2021.08.16 |
[C++] Memory Alignment (메모리 얼라인먼트) & Object In Memory (객체생성) (1) | 2021.08.16 |