rule_of_three — 3/5/0의 규칙
rule_of_three — 3/5/0의 규칙 (Rule of three/five/zero)
**3의 규칙(Rule of Three)**은 "클래스가 사용자 정의 소멸자, 사용자 정의 복사 생성자, 또는 사용자 정의 복사 대입 연산자를 요구한다면, 거의 확실히 세 가지 모두를 요구한다"는 C++ 지침이에요.
리소스를 관리하는 클래스를 만들 때 이 규칙을 지키지 않으면 얕은 복사(shallow copy)로 인한 이중 해제, 메모리 누수 같은 심각한 버그가 생길 수 있어요.
C++는 사용자 정의 타입의 객체를 다양한 상황에서 복사하고 복사 대입해요 (값으로 전달/반환, 컨테이너 조작 등). 접근 가능하다면 이 특수 멤버 함수들이 호출되며, 사용자 정의가 아니면 컴파일러가 암시적으로 정의해요.
암시적으로 정의된 특수 멤버 함수는, 클래스가 핸들이 비클래스 타입 객체(원시 포인터, POSIX 파일 디스크립터 등)인 자원을 관리하고, 그 소멸자가 아무것도 하지 않으며 복사 생성자/대입 연산자가 "얕은 복사"(핸들의 값만 복사하고 기반 자원은 복제하지 않음)를 수행하는 경우에는 사용하면 안 돼요.
출처: cppreference
본문
3의 규칙 (Rule of three)
클래스가 사용자 정의 소멸자, 사용자 정의 복사 생성자, 또는 사용자 정의 복사 대입 연산자를 요구한다면, 거의 확실히 세 가지 모두를 요구해요.
#include <cstddef>
#include <cstring>
#include <iostream>
#include <utility>
class rule_of_three
{
char* cstring; // 동적으로 할당된 메모리 블록의
// 핸들로 쓰이는 원시 포인터
public:
explicit rule_of_three(const char* s = "") : cstring(nullptr)
{
if (s)
{
cstring = new char[std::strlen(s) + 1]; // 할당
std::strcpy(cstring, s); // 채우기
}
}
~rule_of_three() // I. 소멸자
{
delete[] cstring; // 해제
}
rule_of_three(const rule_of_three& other) // II. 복사 생성자
: rule_of_three(other.cstring) {}
rule_of_three& operator=(const rule_of_three& other) // III. 복사 대입
{
// 간결함을 위해 copy-and-swap 으로 구현
// 참고: 이것은 저장 공간 재사용을 방지
rule_of_three temp(other);
std::swap(cstring, temp.cstring);
return *this;
}
const char* c_str() const // 접근자
{
return cstring;
}
};
int main()
{
rule_of_three o1{"abc"};
std::cout << o1.c_str() << ' ';
auto o2{o1}; // II. 복사 생성자 사용
std::cout << o2.c_str() << ' ';
rule_of_three o3("def");
std::cout << o3.c_str() << ' ';
o3 = o2; // III. 복사 대입 사용
std::cout << o3.c_str() << '\n';
} // I. 모든 소멸자가 여기서 호출됨
출력:
abc abc def abc
rule_of_three 클래스는 원시 포인터로 동적 메모리를 관리하므로, 소멸자·복사 생성자·복사 대입 연산자 세 가지를 모두 직접 정의해야 해요. 소멸자에서 delete[], 복사 생성자에서 깊은 복사, 복사 대입에서 copy-and-swap을 구현했어요.
복사 가능한 핸들을 통해 복사 불가 자원을 관리하는 클래스는 복사 생성자와 복사 대입 연산자를 private으로 선언하거나 = delete로 정의해야 할 수 있어요. 이것도 3의 규칙의 또 다른 적용이에요: 하나를 삭제하고 나머지를 암시적으로 정의하도록 두면 보통 잘못된 코드가 돼요.
5의 규칙 (Rule of five)
사용자 정의(그리고 = default 또는 = delete로 선언된 것 포함) 소멸자, 복사 생성자, 복사 대입 연산자가 존재하면 이동 생성자와 이동 대입 연산자의 암시적 정의를 막기 때문에, 이동 의미론이 필요한 어떤 클래스든 다섯 가지 특수 멤버 함수를 모두 선언해야 해요.
0의 규칙 (Rule of zero)
자원을 관리하는 클래스는 자원 관리를 위해 소멸자, 복사/이동 생성자, 복사/이동 대입 연산자 대신 다른 클래스를 소유해야 해요 (단일 책임 원칙). 다른 클래스는 커스텀 소멸자, 복사/이동 생성자, 복사/이동 대입 연산자를 가져선 안 돼요.
이 규칙은 C++ Core Guidelines에도 C.20 "If you can avoid defining default operations, do"로 나타나요.
다형적 클래스는 복사·이동을 방지하기 위해 소멸자를 public이고 가상으로, 특수 멤버 함수는 = default로 정의해야 해요. 또는 복사/이동을 = delete로 억제할 수도 있는데, 이는 다음 일반적인 표현으로 이어져요 — C++ Core Guidelines C.21: "If you define or =delete any copy, move, or destructor function, define or =delete them all."