프로그래밍/C&C++

the rule of 0/3/5

은율실험실 2025. 3. 18. 19:50
반응형

혹시 구조체는 다음과 같은 것들을 구현해야 한다고 들어보신 적이 있으신가요?

  • 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

 

반응형