class를 작성 하다 보면 가장 먼저 하는 고민 중 하나는 부가 적인 생성자의 지원 여부 입니다. Pool을 지원하는 class 혹은 많은 책임을 지고 단일을 유지 해야 하는 Manager class의 경우 데이터의 이동/복사를 허용하지 않는 경우가 많습니다. 때로는 성능이나 noexcept 문제로 이동만 지원하고 복사를 허용하지 않는 경우도 있습니다. 혹은 이동과 복사를 지원해서 std의 container 사용을 원활하게 할 수도 있습니다. 결론은 class의 의도에 따라 이동/복사 생성 대입 연산자가 달라진 다는 것입니다. class를 작성하는 저는 그 의도를 머릿속에 가지고 있습니다. 하지만 안타까운 점은 AI가 발전함에도 아직 제 머릿속을 볼 수 있는 방법은 쉽지 않다는 것일것 같습니다. 의도는 코드에 드러나야 합니다. 특히 그 의도가 런타임에 알아챌 수 있는 것보다 컴파일 타임에 드러나는 것이 더욱 바람직합니다.
코드의 주석은 힘이 없습니다. 보통 assert를 통해서 암시적 검증도 할 수 있지만 코드의 의도를 드러낼 수도 있습니다. 이 두가지 사안을 종합해 볼때 static_assert를 사용해서 의도를 드러내면 가장 좋아 보이네요. 예를 들면 나는 이 class의 이동/복사를 의도하지 않았다. 혹은 이 class의 이동/복사를 추가하려면 내 의도가 아니기 때문에 다시 생각해 봐야 한다를 static_assert로 표현할 수 있어야 합니다. 그러면은 옆 사람에게 일일이 제 코드를 설명하지 않아도 되서 좋을텐데요.
이동 생성 가능한 유형인지 판단하는 trait class가 있습니다. 대표적으로 std::is_move_constructible 입니다. type_trait 헤더에서 참조 할 수 있는데 간단하게 기능을 표로 정리하면 아래와 같습니다.
| 의미 | 타입 트레이트 |
|---|---|
| 복사 생성자 가능 | std::is_copy_constructible_v<T> |
| 복사 대입 연산자 가능 | std::is_copy_assignable_v<T> |
| 이동 생성자 가능 | std::is_move_constructible_v<T> |
| 이동 대입 연산자 가능 | std::is_move_assignable_v<T> |
직접 default 연산자들을 구현 / delete 하는 코드를 만들어서 확인해보겠습니다. 언리얼 엔진의 내부 코드에 괜찮은 #define이 있어서 해당 코드를 참조해서 작성 해봤습니다.
#include
#define HD_DISABLE_COPY(_name) _name(const _name&) = delete
#define HD_DISABLE_MOVE(_name) _name(_name&&) = delete
#define HD_DISABLE_ASSIGNMENT(_name) _name& operator=(const _name&) = delete
#define HD_DISABLE_MOVE_ASSIGNMENT(_name) _name& operator=(_name&&) = delete
#define HD_ENABLE_COPY(_name) _name(const _name&) = default
#define HD_ENABLE_MOVE(_name) _name(_name&&) = default
#define HD_ENABLE_ASSIGNMENT(_name) _name& operator=(const _name&) = default
#define HD_ENABLE_MOVE_ASSIGNMENT(_name) _name& operator=(_name&&) = default
class DisableClass
{
public:
DisableClass( void ) noexcept = default;
HD_DISABLE_COPY( DisableClass );
HD_DISABLE_MOVE( DisableClass );
HD_DISABLE_ASSIGNMENT( DisableClass );
HD_DISABLE_MOVE_ASSIGNMENT( DisableClass );
};
class EnableClass
{
public:
EnableClass(void) noexcept = default;
HD_ENABLE_COPY(EnableClass);
HD_ENABLE_MOVE(EnableClass);
HD_ENABLE_ASSIGNMENT(EnableClass);
HD_ENABLE_MOVE_ASSIGNMENT(EnableClass);
};
static_assert result
해당 방식으로 코드에서 의도를 드러낼 수 있습니다. 물론 이 의도는 사소하고 특히 개인 작업에서는 큰 의미가 없을 수도 있습니다. 하지만 여러 사람과 코드를 작성하는 경우에는 어느 정도 의미가 있을 수도 있다고 생각합니다. 주석보다는 강력하고 무엇보다 컴파일 타임에 해당 의도를 알 수 있다는 것이 장점일 수 있어보이네요!
해당 template의 구현 원리는 declval에 있습니다. declval 기능은 실제로 생성자를 호출해서 객체를 만들지 않고도 타입 의 rvalue 참조(T&&)를 확인할 수 있는 기능입니다. 특징은 선언은 있지만 정의가 없어서 호출하면 에러가 발생합니다. 사용은 decltype(std::declval