the rule of 0/3/5
혹시 구조체는 다음과 같은 것들을 구현해야 한다고 들어보신 적이 있으신가요?
- destructor
- copy constructor
- move constructor
- copy assignment
- move assignment
오늘은 이것들이 무엇인지, 또 왜 필요한지를 한번 같이 살펴보도록 할게요.
좌측값(lvalue)은 표현식이 끝나도 계속 사용이 가능한 값을 의미해요.
auto lvalue {"69"};
std::cout < lvalue << std::endl;
우측값(rvalue)은 표현식이 끝나면 더 이상 사용이 불가능한 값을 의미해요.
std::cout < "rvalue" << std::endl;
소멸자(destructor)는 생명주기(life cycle)가 끝날 경우(e.g. scope) 자동으로 실행되는 함수예요.
#include <iostream>
struct foo
{
foo()
{
std::cout << "constructor" << std::endl;
}
~foo()
{
std::cout << "destructor" << std::endl;
}
};
auto main() -> int
{
foo bar;
std::cout << "hello world" << std::endl;
}
// constructor
// hello world
// destructor
소멸자는 주로 동적(dynamically)으로 할당된 heap 메모리를 해제하기 위해서 작성해요.
#include <cstring>
#include <iostream>
struct foo
{
char* ptr;
foo()
:
ptr(new char[4])
{
std::memcpy(this->ptr, "foo", 4);
}
~foo()
{
delete[] this->ptr; // free memory!
}
};
auto main() -> int
{
foo bar;
}
복사 생성자(copy constructor)는 좌측값을 인수로 받아서 값을 복사(deep copy)하는 생성자예요.
#include <cstring>
#include <iostream>
struct foo
{
char* ptr {nullptr};
foo(char* ptr)
{
this->ptr = new char[std::strlen(ptr)];
std::memcpy(this->ptr, ptr, std::strlen(ptr));
}
foo(foo& other)
{
this->ptr = new char[std::strlen(other.ptr)];
std::memcpy(this->ptr, other.ptr, std::strlen(other.ptr));
}
};
auto main() -> int
{
foo a {"69"}; foo b {a};
std::cout << "content:" << std::endl;
std::cout << a.ptr << std::endl; // 69
std::cout << b.ptr << std::endl; // 69
std::cout << "address:" << std::endl;
std::cout << (void*) a.ptr << std::endl; // 0x505358
std::cout << (void*) b.ptr << std::endl; // 0x505368
}
이때 만약 적절치 않은 복사 이후 원본의 메모리가 해제되는 경우 dangling pointer 문제가 발생할 수 있어요.
#include <cstring>
#include <iostream>
struct foo
{
char* ptr {nullptr};
foo(char* ptr)
{
this->ptr = new char[std::strlen(ptr)];
std::memcpy(this->ptr, ptr, std::strlen(ptr));
}
foo(foo& other)
{
this->ptr = other.ptr; // <-- we are only copying the address, not the data
}
};
auto main() -> int
{
foo a {"69"}; foo b {a};
std::cout << "content:" << std::endl;
std::cout << a.ptr << std::endl; // 69
std::cout << b.ptr << std::endl; // 69
std::cout << "address:" << std::endl;
std::cout << (void*) a.ptr << std::endl; // 0x505358
std::cout << (void*) b.ptr << std::endl; // 0x505358
}
이동 생성자(move constructor)는 우측값을 인수로 받아서 포인터를 강탈(steal)하는 생성자예요.
#include <cstring>
#include <utility>
#include <iostream>
struct foo
{
char* ptr {nullptr};
foo(char* ptr)
{
std::cout << "create" << std::endl;
this->ptr = new char[std::strlen(ptr)];
std::memcpy(this->ptr, ptr, std::strlen(ptr));
}
foo(foo&& other)
{
std::cout << "move" << std::endl;
// steal
this->ptr = other.ptr;
// nullify
other.ptr = nullptr;
}
};
auto main() -> int
{
foo a {std::move(foo {"69"})};
}
우측값은 표현식에 사용 후 곧바로 해제되는 까닭에 포인터만 교환해도 효율적으로 구조체를 복사할 수 있어요.
복사 대입자(copy assignment)와 이동 대입자(move constructor)는 각각 좌측값과 우측값의
대입 연산자를 오버로딩한다는 것만 제외하면 각각 복사 생성자 및 이동 생성자와 동일해요.
the rule of 0
모든 필드가 적절한 소멸자와 이동 및 복사를 지원하는 자료형 또는 구조체만을 갖는 경우에 적합해요.
the rule of 0
- destructor
- copy constructor
- move constructor
- copy assignment
- move assignment
the rule of 3
일부의 필드가 적절한 소멸자와 이동 및 복사를 지원하지 않는 자료형 또는 구조체일 경우에 적합해요.
the rule of 5
- destructor
- copy constructor
- move constructor
- copy assignment
- move assignment
the rule of 5
일부의 필드가 적절한 소멸자와 이동 및 복사를 지원하지 않는 자료형 또는 구조체일 경우에 적합해요.
the rule of 5
- destructor
- copy constructor
- move constructor
- copy assignment
- move assignment
copy-and-swap idiom
the rule of 3을 구현하는 방법의 일종이에요.
다만 lvalue를 복사 생성 및 대입 이후에도 사용할 수 있도록 하기 위해서 항상 pass by value를 사용하기에
논리를 간략화하고 유지보수를 수월하게 하는 장점이 있지만 성능은 비교적 뒤처질 수 있다는 단점이 있어요.
class example
{
// ...
friend void swap(example& lhs, example& rhs)
{
// enable ADL
using std::swap;
swap(lhs.foo rhs.foo);
swap(lhs.bar, rhs.bar);
}
// copy constructor
example(example rhs)
{
if (*this != rhs)
{
swap(*this, other);
}
}
// move constructor
example(example&& rhs)
{
if (*this != rhs)
{
swap(*this, other);
}
}
// copy assignment
auto operator=(example rhs) -> example&
{
if (*this != rhs)
{
swap(*this, other);
}
return *this;
}
// move assignment
auto operator=(example&& rhs) -> example&
{
if (*this != rhs)
{
swap(*this, other);
}
return *this;
}
};
Credits
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