C++과 친해지기

Fight Club - Rule of 0/3/5

NAEU 2025. 12. 13. 11:25

C++을 공부하다 보면
Rule of 5, Rule of 3, Rule of 0라는 뭔 규칙을 의미하는건가 싶은 것을 볼 수 있다
오늘은 이 Rule of 0/3/5 에 대해 알아보자

사실 Rule of 3와 5은 동일하다
정확히하자면 기존의 Rule of 3에서 C++11 이후부턴 2가지의 규칙이 추가되어 5가 된것이다
이 이야기는 살짝 미뤄두고 Rule of 0부터 확인해보겠다

Rule of 0
핵심만 읊자면 클래스가 직접 메모리,파일,핸들 등을 관리하지 않게 설계하라는 의미이다
아니 그럼 데이터들을 어떻게 관리하냐? -> RAII를 사용하면 된다
RAII는 Resource acquisition is initialization의 약자로, "자원 획득은 초기화"라는 의미이다
RAII는 자원은 해당 객체가 살아있을 때만 보유되는 것을 보장하는 규칙이다
따라서 객체 누수가 없다면 그 안에 있는 자원 누수도 없어지니 자원 관리가 간편해진다
쉬운 예시로는 std::vector가 있다
std::vector는 내부적으로 Heap에 메모리를 할당하는 동적 할당을 사용한다
그렇기에 사용이 완료된 후에는 할당한 메모리를 해제해주어야하는데, std::vector는 이를 소멸자에서 처리하게 두어 사용자가 std::vector를 사용할때 굳이 신경쓰지 않아도 되게끔 만들었다
이게 Rule of 0의 좋은 예시이다
가장 안전하고, 버그가 좋고, 유지보수 비용이 낮다
Rule of 3
그렇다면 클래스가 리소스를 직접 관리할때는 어떤 것들이 필요할까?
해당 클래스 혹은 타입에 대해서 무조건
복사 생성자
복사 대입 연산자
소멸자
이 3개를 정의해야한다라고 말하는 것이 이 Rule of 3이다
class Data {
public:
Data(size_t size): m_data(new int[size]), m_size(size) {}

private:
int* m_data;
size_t m_size;
};

기본적인 데이터가 이렇게 존재할때 우선 이 데이터가 사라질때 만들어둔 m_data를 메모리에서 해제시켜주어야한다
class Data {
public:
Data(size_t size): m_data(new int[size]), m_size(size) {}

//소멸자
~Data() {
delete[] m_data;
}

private:
int* m_data;
size_t m_size;
};

그리고 나선
Data newData(existData);
Data newData = existData;
방식으로 값을 복사해서 사용하고 싶다면
class Data {
public:
Data(size_t size): m_data(new int[size]), m_size(size) {}

//복사 생성자
Data(const Data& other): m_data(new int[other.m_size]), m_size(other.m_size) {
std::copy(other.m_data, other.m_data + m_size, m_data);
}

//복사 대입 연산자
Data& operator=(const Data& other) {
if (this != &other) {
delete[] m_data;
m_size = other.m_size;
m_data = new int[m_size];
std::copy(other.m_data, other.m_data + m_size, m_data);
}
return *this;
}

//소멸자
~Data() {
delete[] m_data;
}

public:
int* m_data;
size_t m_size;
};

뭐 둘의 차이라고 한다면 복사 생성은 데이터를 처음 생성할때 기존의 있는 데이터 값을 복사해서 생성한다는 것이고,
복사 대입 연산자는 이미 있는 데이터의 값을 다른 데이터로 바꾸고 싶을때 복사해서 사용한다는 차이가 있다
이렇게 3개의 규칙을 알아봤는데 C++11 이후부턴 드디어 그 녀석이 생겨났다
Rule of 5
std::move();
포인터가 C++ 뉴비들의 첫 벽이라면 얘는 뉴비 분쇄기다
이동 시멘틱에 대해선 추후에 자세히 다루기로 하고 우선 나머지 2개의 규칙을 알아보자
이동 생성자
이동 대입 연산자
move라는 이동이 추가되면서 더 이상 비효율적인 복사 대신에 전체를 욺겨버릴 수 있는 효율적인 방법이 생겨났다
class Data {
public:
Data(size_t size): m_data(new int[size]), m_size(size) {}

//이동 생성자
Data(Data&& other): m_data(new int[other.m_size]), m_size(other.m_size) {
other.m_data = nullptr;
other.m_size = 0;
}

//이동 대입 연산자
Data& operator=(Data&& other) {
if (this != &other) {
delete[] m_data;
m_size = other.m_size;
m_data = new int[m_size];
std::copy(other.m_data, other.m_data + m_size, m_data);

  other.m_data = nullptr;
  other.m_size = 0;
  }

return *this;
}

//소멸자
~Data() {
delete[] m_data;
}

public:
int* m_data;
size_t m_size;
};

복사와 큰 차이점을 짚는다면 변수를 받아올 때 &&(Rvalue 참조 선언자)로 받아온다는 것과
other.m_data = nullptr;
other.m_size = 0;
로 기존에 있던 값들을 리셋한다는 것이다

앞으로 데이터를 정의할때 이 5가지를 꼭 기억해보자

'C++과 친해지기' 카테고리의 다른 글

Spider-Man: Into the Spider-Verse - 멀티스레딩  (0) 2026.01.07