Rule of Three / Five / zero
C++의 class 정의에 대한 몇 가지 규칙을 소개한다.
Rule of Three
정의
클래스에서 아래 세 가지 중 하나라도 명시적으로 정의한 것이 있다면 나머지 두가지도 명시적으로 정의할 것.
소멸자, 복사 생성자, 복사 대입 연산자 중 하나라도 명시적으로 정의 했다면 대부분의 경우 셋다 있어야 한다는 뜻
( 소멸자가 명시적으로 작성되면, 이동 생성자/대입연산자를 자동으로 생성해 주지 않는다 )
- 소멸자만 작성할 경우 정상 작동하지만 이동 시맨틱을 사용하지 않게 되는 큰 Loss가 발생할 수 있음.
- Base class로 사용하기 위하여 가상 소멸자를 선언하는 경우도 역시 포함.
- 소멸자(Destructor)
- 복사 생성자(Copy Constructor)
- 복사 대입 연산자(Copy Assignment Operator)
예시) 사용자 정의 소멸자(User-defined Destructor)만 정의되어 있다면,
사용자 정의 복사 생성자(User-defined Copy Constructor)와
사용자 정의 복사 대입 연산자(User-defined Copy Assignment Operator)도 함께 정의해야 한다.
이유
사용자 정의 타입의 객체(Object)를 복사 및 복사 할당하는
다양한 상황(passing, returning by value, manipulating a container 등)에서
특수 멤버 함수들(복사 생성자, 복사 할당 연산자, 소멸자)이 호출된다.
위에서 언급한 특수 멤버 함수들을 유저가 정의하지 않을 경우 컴파일러에 의해 암시적으로 정의된다.
상황
- 컴파일러에 의해 암시적으로 정의된 특수 멤버 함수들로 인하여 잘못된 동작을 하는 상황
만약 사용자가 정의한 class가 어떤 자원을 관리하는데
그 자원과 연결된 핸들의 소멸자가 아무것도 하지 않는
non-class type( Raw Pointer, POSIX file discriptor 등.. )의 객체라면?
(간단히 말하면 포인터를 멤버로 가지고 있고, 그 포인터에 메모리 할당을 해버리는 방식 )
이러한 사용자 정의 class 객체가 복사 또는 복사 대입될 경우
Copy Constructor나 Copy Assignment Operator는 "Shallow Copy"를 수행할 것이다.
Shallow Copy는 객체의 핸들에 대한 복사만 일어날 뿐 핸들이 참조하고 있는 자원에 대한 복사는 하지 않는다.
( 예시 코드는 이곳을 참고 )
Rule of Five
정의
사용자 정의 소멸자, 복사 생성자, 복사 할당 연산자의 존재는
이동 생성자와, 이동 할당 연산자의 암묵적 정의를 방해한다.
따라서 Move semantics이 필요한 경우 아래의 다섯 가지 특수 멤버 함수 모두를 선언해야한다.
- 소멸자(Destructor)
- 복사 생성자(Copy Constructor)
- 복사 대입 연산자(Copy Assignment Operator)
- 이동 생성자(Move Constructor)
- 이동 대입 연산자(Move Assignment Operator)
예를 들어 사용자 정의 소멸자로 인해 이동 생성자 또는 이동 연산자의 암시적 생성이 막힌 경우가 있다.
예시 코드
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;
}*/
};
이유
Rule of Three와 달리 이동 생성자와 이동 할당을 제공하지 못하는 것은
대개 오류가 아니라 최적화 기회를 놓치는 것이다.
Rule of Zero
정의
소멸자, 복사 생성자, 복사 할당 연산자 모두를 명시적으로 만들지 않으면
컴파일러가 모두 자동으로 만든다.
이를 CGF( Compiler-generated Functions ) 라고 한다.
핵심
C++에서는 유사한 방식으로 여러가지의 규칙을 정의하고, 구현에 관한 가이드라인을 제공한다.
각각의 규칙에는 장단점과 많은 논의가 있다.
하지만 이 규칙들에서 말하고자하는 명백한 것은
“소멸자만 따로 정의함으로 인해 컴파일러가 '이동 생성자/대입 연산자'를 자동 생성하지 못하게 하는 것을 방지하라.”
는 것이다.
즉, '이동 시맨틱을 지원하지 않는 클래스를 만들지 말라' 는 것.
다르게 말하면 구식 스타일의 메모리 할당을 피하라는 것이다.
또한 가능하면 Raw Pointer를 사용하지 말 것.
Object** 같은 2차원 포인터보다는
메모리 할당 해제를 알아서 해주는 std::vector<std::vector<Object>> 와 같은 방식을 권장하는 것이다.
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