C++ = default와 = delete: 특수 멤버 함수 제어와 복사 금지·이동 전용 타입
이 글의 핵심
복사 생성자를 delete했더니 이동도 함께 사라지는 문제처럼, 특수 멤버 함수는 하나를 선언하면 다른 함수의 암시적 생성 여부가 바뀝니다. = default가 noexcept와 트리비얼 타입 여부에 주는 영향, 추상 클래스에서 가상 소멸자를 default로 두는 방법, 모호한 오버로드를 delete로 막는 패턴을 설명합니다.
default와 delete란?
특수 멤버 함수를 명시적으로 제어
class MyClass {
public:
MyClass() = default; // 기본 생성자 생성
MyClass(const MyClass&) = delete; // 복사 생성자 삭제
};
C++ 컴파일러는 클래스에 기본 생성자, 복사 생성자, 복사 대입, 이동 생성자, 이동 대입, 소멸자라는 여섯 개의 특수 멤버 함수를 필요할 때 자동으로 만들어 줍니다. 편리하지만 규칙이 복잡합니다. 어떤 함수를 직접 선언하면 다른 함수의 자동 생성이 조용히 꺼지기 때문입니다. 예를 들어 생성자를 하나라도 직접 만들면 기본 생성자가 사라지고, 복사 생성자나 소멸자를 선언하면 이동 연산이 생성되지 않습니다. = default와 = delete는 이 암묵적 규칙에 기대지 않고 “이 함수는 컴파일러 기본 구현으로 만들어라”, “이 함수는 존재하지 않는 것으로 취급하라”를 코드에 명시하는 도구입니다.
C++11 이전에는 복사를 막으려고 복사 생성자를 private에 선언만 하고 정의하지 않는 요령을 썼습니다. 이 방식은 클래스 밖에서는 “is private” 에러가 나지만, 클래스 내부나 friend에서 실수로 복사하면 링크 단계에서야 “undefined reference” 에러가 나서 원인을 찾기 어려웠습니다. = delete는 어디서 호출하든 컴파일 단계에서 “use of deleted function”으로 즉시 막아 줍니다.
= default
class Point {
private:
int x, y;
public:
// 기본 생성자
Point() = default;
// 복사 생성자
Point(const Point&) = default;
// 복사 대입 연산자
Point& operator=(const Point&) = default;
// 이동 생성자
Point(Point&&) = default;
// 이동 대입 연산자
Point& operator=(Point&&) = default;
// 소멸자
~Point() = default;
};
이 Point는 여섯 개를 모두 = default로 적었지만, 사실 아무것도 적지 않은 것과 동작이 같습니다. 컴파일러가 어차피 똑같이 만들어 주기 때문입니다. 그러면 왜 적을까요? 하나라도 직접 선언하는 순간 나머지의 자동 생성 규칙이 바뀌므로, 소멸자를 virtual로 만들거나 로깅을 넣으려고 하나를 선언해야 할 때 나머지를 = default로 되살리는 용도로 쓰는 것이 주된 목적입니다. 모든 것을 기본값으로 둘 거라면 아무것도 선언하지 않는 Rule of Zero가 더 간결합니다.
= default에는 어디에 쓰느냐에 따른 차이도 있습니다. 클래스 안에서 첫 선언에 = default를 쓰면 “사용자가 제공하지 않은” 함수로 취급되어, 멤버가 모두 트리비얼하다면 그 클래스는 트리비얼 타입으로 남습니다. 반면 헤더에 Point();로 선언하고 .cpp에서 Point::Point() = default;로 정의하면 “사용자가 제공한” 함수가 되어 트리비얼하지 않게 되고, Point p{};의 값 초기화 동작도 달라집니다. 대신 .cpp에 두면 멤버 타입의 정의를 헤더에 노출하지 않아도 되어 Pimpl 관용구에서 unique_ptr<Impl> 소멸자를 정의할 때 이 방식을 씁니다.
= delete
class NonCopyable {
public:
NonCopyable() = default;
// 복사 금지
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 이동 허용
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
복사를 막고 이동만 허용하는 이 조합이 std::unique_ptr, std::thread, std::fstream 같은 단독 소유 타입의 기본 형태입니다. 이렇게 만든 타입을 복사하려 하면 GCC는 “use of deleted function ‘NonCopyable::NonCopyable(const NonCopyable&)’” 에러를 냅니다. 에러 메시지에 나오는 함수가 직접 삭제한 것이 아니라 암시적으로 삭제된 것일 때도 있는데, 예를 들어 unique_ptr 멤버를 가진 클래스의 복사 생성자는 멤버를 복사할 수 없어서 자동으로 삭제됩니다. 이때 GCC와 Clang은 “implicitly deleted because … has a deleted copy constructor”라는 보충 설명으로 어떤 멤버가 원인인지 알려 줍니다.
맨 첫 줄의 NonCopyable() = default;도 필수입니다. 복사 생성자를 선언하는 순간(삭제 선언도 포함) 컴파일러는 기본 생성자를 만들지 않으므로, 이 줄이 없으면 NonCopyable obj;가 “no matching function for call to ‘NonCopyable::NonCopyable()‘“로 실패합니다.
실전 예시
예시 1: 싱글톤
class Singleton {
private:
static Singleton* instance;
// private 생성자
Singleton() = default;
public:
// 복사/이동 금지
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton* getInstance() {
if (!instance) {
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
싱글톤에서 복사와 이동을 모두 삭제하는 이유는 Singleton copy = *Singleton::getInstance();처럼 두 번째 인스턴스가 만들어지는 경로를 막기 위해서입니다. 다만 이 예제의 getInstance()는 스레드 안전하지 않습니다. 두 스레드가 동시에 if (!instance)를 통과하면 객체가 두 개 만들어지고 하나는 누수됩니다. C++11부터는 함수 안의 static 지역 변수 초기화가 스레드 안전하게 보장되므로 static Singleton& getInstance() { static Singleton s; return s; }(Meyers 싱글톤)가 더 간결하고 안전합니다. 참고로 복사 생성자를 삭제하면 이동 생성자는 자동으로 선언되지 않으므로 이동 쪽 두 줄은 없어도 동작은 같지만, 의도를 드러내기 위해 적어 두는 경우가 많습니다.
예시 2: 리소스 관리
class FileHandle {
private:
FILE* file;
public:
explicit FileHandle(const char* filename) {
file = fopen(filename, "r");
}
~FileHandle() {
if (file) {
fclose(file);
}
}
// 복사 금지 (파일 핸들은 복사 불가)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 이동 허용
FileHandle(FileHandle&& other) noexcept : file(other.file) {
other.file = nullptr;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (file) {
fclose(file);
}
file = other.file;
other.file = nullptr;
}
return *this;
}
};
파일 핸들을 복사할 수 있다면 두 객체가 같은 FILE*를 들고 있다가 둘 다 소멸자에서 fclose를 호출해 이중 해제가 됩니다. 컴파일러가 자동 생성하는 복사 생성자는 포인터 값만 복사하는 얕은 복사라서, 복사 금지를 명시하지 않으면 이 버그가 조용히 만들어집니다. 이동 연산에서는 원본의 file을 nullptr로 비워 두는 것이 핵심이며, 소멸자의 if (file) 검사와 짝을 이룹니다. 이동 연산에 noexcept를 붙인 이유는 아래 “심화: = default와 noexcept”에서 설명합니다.
이 예제에서는 fopen이 실패해 nullptr을 반환하는 경우를 처리하지 않습니다. 실무 코드라면 생성자에서 예외를 던지거나 isOpen() 같은 확인 함수를 제공해야 합니다. 사실 이 정도 래퍼라면 std::unique_ptr<FILE, decltype(&fclose)>로 직접 클래스를 만들지 않고도 같은 효과를 얻을 수 있어, Rule of Zero로 가는 편이 코드가 적습니다.
예시 3: 특정 타입 금지
class SafeInt {
private:
int value;
public:
SafeInt(int v) : value(v) {}
// double 변환 금지
SafeInt(double) = delete;
int getValue() const {
return value;
}
};
int main() {
SafeInt x(10); // OK
// SafeInt y(3.14); // 에러: delete된 생성자
}
삭제된 함수도 오버로드 해석에는 참여한다는 점이 이 기법의 원리입니다. SafeInt(3.14)를 호출하면 컴파일러는 int 버전(변환 필요)과 double 버전(정확히 일치) 중 double 버전을 고르고, 고른 함수가 삭제되어 있으므로 에러를 냅니다. 삭제하지 않았다면 3.14가 조용히 3으로 잘렸을 것입니다. float도 double로의 승격이 int 변환보다 우선하므로 함께 막힙니다.
한계도 있습니다. SafeInt(10L)처럼 long을 넘기면 long → int와 long → double이 둘 다 “변환”이라 순위가 같아 모호한 호출 에러가 납니다. 정확히 int만 허용하고 나머지 모든 타입을 막으려면 template<typename T> SafeInt(T) = delete;를 추가하는 방식이 더 확실합니다. 템플릿은 모든 타입에 대해 정확히 일치하므로, int가 아닌 인자는 전부 삭제된 템플릿으로 가고 int만 비템플릿 버전이 우선 선택됩니다.
예시 4: 힙 할당 금지
#include <cstddef>
class StackOnly {
public:
StackOnly() = default;
// 힙 할당 연산자만 삭제 (placement new 등은 별도 규칙)
void* operator new(std::size_t) = delete;
void* operator new[](std::size_t) = delete;
void operator delete(void*) = delete;
void operator delete[](void*) = delete;
};
int main() {
StackOnly obj; // OK: 자동 저장 기간
// auto* p = new StackOnly(); // 컴파일 에러
}
주의: 클래스의 operator new를 삭제해도 막히는 것은 new StackOnly 표현식뿐입니다. std::vector<StackOnly>는 std::allocator를 통해 전역 ::operator new로 메모리를 받고 그 위에 객체를 생성하므로 정상적으로 컴파일되고 요소는 힙에 놓입니다. ::new StackOnly처럼 전역 연산자를 명시해도 우회되고, std::make_unique<StackOnly>()는 내부적으로 new StackOnly를 쓰므로 막힙니다. 즉 이 기법은 “힙에 절대 놓이지 않음”을 보장하는 것이 아니라 실수로 new를 쓰는 것을 막는 정도의 장치이므로, 의도를 주석으로 남기세요. RAII 가드(락 가드, 스코프 타이머)처럼 스코프에 묶여야 의미가 있는 타입에 주로 씁니다.
Rule of Five
class Resource {
private:
int* data;
public:
// 1. 생성자
Resource(int value) : data(new int(value)) {}
// 2. 소멸자
~Resource() {
delete data;
}
// 3. 복사 생성자
Resource(const Resource& other)
: data(new int(*other.data)) {}
// 4. 복사 대입 연산자
Resource& operator=(const Resource& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
// 5. 이동 생성자
Resource(Resource&& other) noexcept
: data(other.data) {
other.data = nullptr;
}
// 6. 이동 대입 연산자
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
Rule of Five는 “소멸자, 복사 생성자, 복사 대입, 이동 생성자, 이동 대입 중 하나를 직접 정의해야 한다면 다섯 개 모두를 검토하라”는 규칙입니다. 위 코드는 번호를 여섯 개 붙였지만 1번 일반 생성자는 다섯 개에 포함되지 않습니다. 하나를 직접 정의했다는 것은 그 클래스가 원시 자원(여기서는 new로 받은 메모리)을 직접 관리한다는 뜻이고, 그렇다면 나머지 연산의 기본 동작(포인터 얕은 복사)은 거의 확실히 틀렸기 때문입니다.
이 구현의 복사 대입 연산자에는 교과서적인 약점이 있습니다. delete data;를 먼저 하고 new int(...)를 하는데, new가 std::bad_alloc을 던지면 data는 이미 해제된 주소를 가리킨 채 남고, 나중에 소멸자가 그것을 다시 delete해 이중 해제가 됩니다. 새 자원을 먼저 할당한 다음 기존 자원을 해제하거나, 복사본을 만들어 swap하는 copy-and-swap 관용구를 쓰면 예외 안전성이 보장됩니다. 다섯 개를 모두 정확히 작성하기가 이렇게 까다롭다는 것이 다음 절의 Rule of Zero가 권장되는 이유입니다.
Rule of Zero
// ✅ Rule of Zero: 스마트 포인터 사용
class Resource {
private:
std::unique_ptr<int> data;
public:
Resource(int value) : data(std::make_unique<int>(value)) {}
// 특수 멤버 함수 불필요
// 컴파일러가 자동 생성
};
Rule of Zero는 “자원 관리는 그 일만 하는 클래스(스마트 포인터, 컨테이너, 파일 스트림)에 맡기고, 나머지 클래스는 특수 멤버 함수를 하나도 선언하지 않는다”는 원칙입니다. 위 Resource는 unique_ptr 덕분에 소멸자를 쓰지 않아도 메모리가 해제되고, 이동은 자동으로 생성되며, 복사는 unique_ptr이 복사 불가라서 자동으로 삭제됩니다. 앞의 Rule of Five 버전과 달리 깊은 복사가 필요하다면 복사 생성자만 직접 정의해야 하는데, 이때부터 다시 나머지를 검토해야 하므로 깊은 복사 요구가 있는 경우에는 std::vector처럼 값 의미를 가진 멤버로 바꿀 수 있는지 먼저 생각해 보는 편이 좋습니다.
자주 발생하는 문제
문제 1: 복사 금지 후 이동 불가
// ❌ 이동도 불가능
class MyClass {
public:
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
// 이동 생성자도 암시적으로 삭제됨!
};
// ✅ 이동 명시적 허용
class MyClass {
public:
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
};
정확히 말하면 첫 번째 클래스에서 이동 생성자는 “삭제”되는 것이 아니라 아예 선언되지 않습니다. 복사 생성자나 복사 대입, 소멸자를 사용자가 선언하면(= delete, = default 포함) 컴파일러는 이동 연산을 암시적으로 만들지 않습니다. 그러면 MyClass b = std::move(a);는 이동 생성자를 찾지 못하고 대신 const MyClass&를 받는 복사 생성자를 후보로 고르는데, 그 복사 생성자가 삭제되어 있으니 “use of deleted function ‘MyClass::MyClass(const MyClass&)’” 에러가 납니다. 이동하려 했는데 에러 메시지는 복사 생성자를 가리키기 때문에 처음 보면 혼란스럽습니다.
더 교묘한 경우는 복사를 삭제하지 않고 소멸자만 정의했을 때입니다. 이때도 이동 연산이 생성되지 않지만 복사 생성자는 (폐지 예정인 규칙으로) 여전히 생성되므로, std::move가 에러 없이 조용히 복사로 바뀝니다. 로깅 한 줄을 넣으려고 소멸자를 추가했다가 vector 재할당 성능이 크게 떨어지는 일이 이렇게 생깁니다. 두 예제 모두 기본 생성자가 없다는 점도 알아 두세요. 복사 생성자를 선언했으므로 MyClass obj;는 컴파일되지 않고, 필요하면 MyClass() = default;를 추가해야 합니다.
문제 2: default와 멤버 초기화
// ❌ 멤버 초기화 필요
class MyClass {
int x; // 초기화 안 됨
public:
MyClass() = default;
};
// ✅ 멤버 초기화
class MyClass {
int x = 0; // 멤버 초기화
public:
MyClass() = default;
};
= default 기본 생성자는 컴파일러가 만드는 기본 생성자와 똑같이 동작하는데, 컴파일러의 기본 생성자는 int 같은 기본 타입 멤버를 초기화하지 않습니다. 그래서 첫 번째 클래스로 MyClass m;을 만들면 m.x는 쓰레기 값이고, 이를 읽는 것은 미정의 동작입니다. 디버그 빌드에서는 0이나 0xCCCCCCCC(MSVC) 같은 값이 나와 우연히 동작하다가, 릴리스 빌드에서 결과가 달라지는 전형적인 버그가 됩니다. 흥미롭게도 MyClass m{};처럼 중괄호로 값 초기화하면 기본 생성자가 사용자 제공이 아니므로 x가 0으로 초기화됩니다. 이런 미묘한 차이에 기대기보다 두 번째 예제처럼 멤버 선언에 기본값을 적는 것이 가장 확실합니다.
문제 3: 가상 소멸자
// ❌ 가상 소멸자 필요
class Base {
public:
~Base() = default; // 가상 아님
};
// ✅ 가상 소멸자
class Base {
public:
virtual ~Base() = default;
};
Base* p = new Derived; delete p;처럼 기반 클래스 포인터로 파생 객체를 삭제하는 코드가 있다면 소멸자는 반드시 virtual이어야 합니다. 그렇지 않으면 Base의 소멸자만 호출되어 Derived의 멤버가 정리되지 않으며, 표준상 미정의 동작입니다. 자세한 증상과 해결은 가상 소멸자 에러 글에서 다룹니다. 가상 소멸자를 선언하면 그것도 “사용자 선언 소멸자”라서 이동 연산의 자동 생성이 꺼집니다. 다형 기반 클래스는 보통 복사·이동을 막는 것이 슬라이싱 방지에도 좋지만, 이동이 필요하다면 문제 1처럼 = default로 되살려야 합니다.
// 특정 오버로드 금지
void func(int x) {
std::cout << "int: " << x << std::endl;
}
void func(double x) = delete; // double 버전 금지
int main() {
func(10); // OK
// func(3.14); // 에러: delete된 함수
}
템플릿 특수화 금지
template<typename T>
void process(T value) {
std::cout << "일반 타입" << std::endl;
}
// double 특수화 금지
template<>
void process<double>(double) = delete;
int main() {
process(10); // OK
// process(3.14); // 에러: delete된 특수화
}
default vs 명시적 구현
// A안: 멤버 기본값 + default 생성자 (흔히 권장)
class PointA {
int x = 0, y = 0;
public:
PointA() = default;
};
// B안: 초기화 리스트가 꼭 필요한 경우만 명시 구현
class PointB {
int x, y;
public:
PointB() : x(0), y(0) {}
};
// default가 유리한 경우: 특별한 로직 없이 멤버 규칙만 따를 때
심화: = default와 noexcept·트리비얼 타입
이동 연산이 noexcept가 아니면 표준 컨테이너가 복사를 선택하는 등 비용이 커질 수 있습니다. std::vector는 재할당할 때 강한 예외 보장을 지키기 위해 std::move_if_noexcept를 쓰는데, 이동 생성자가 예외를 던질 수 있다고 선언되어 있으면(그리고 복사가 가능하면) 이동 대신 복사를 합니다. 이동이 예외를 던지지 않는다면 = default와 함께 noexcept를 명시하는 것이 안전합니다.
사실 = default로 만든 이동 연산은 모든 멤버의 이동이 noexcept라면 자동으로 noexcept가 됩니다. 명시적으로 적는 이유는 두 가지입니다. 하나는 의도를 문서화하는 것이고, 다른 하나는 나중에 noexcept가 아닌 멤버가 추가됐을 때 이를 드러내는 것입니다. C++20부터는 명시한 noexcept가 암시적 명세와 달라도 적은 대로 적용되므로, 실제로는 예외를 던질 수 있는 멤버가 있는데 noexcept로 선언하면 예외가 나는 순간 std::terminate로 프로그램이 종료된다는 점도 기억해야 합니다. static_assert(std::is_nothrow_move_constructible_v<Buffer>);를 한 줄 넣어 두면 이런 변화를 컴파일 타임에 잡을 수 있습니다.
// 타입 정의
struct Buffer {
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
};
트리비얼 타입은 컴파일러가 최적화하기 좋습니다. = default로 의도적으로 “특별한 일 없음”을 표현하면 리뷰어가 Rule of Zero/Five를 판단하기 쉽습니다.
심화: 인터페이스에서 = default (추상 클래스)
순수 가상 함수만 있는 클래스라도 가상 소멸자는 = default로 둘 수 있습니다.
struct IPlugin {
virtual ~IPlugin() = default;
virtual void run() = 0;
};
심화: = delete로 모호한 오버로드·변환 차단
struct Index {
explicit Index(int v) : v_(v) {}
Index(double) = delete; // 암시적 double → int 실수 방지
private:
int v_{};
};
템플릿에서 특정 타입만 막을 때도 delete된 오버로드·특수화가 유용합니다(본문의 템플릿 예시 참고).
심화: 성능·코드 크기
delete된 함수는 ODR 사용 금지를 컴파일 타임에 강제할 뿐, 바이너리에 본문이 들어가지 않습니다. default된 특수 멤버는 암시적 생성과 동일하게 컴파일러가 처리하며, 수동 구현보다 작고 빠른 기계어가 나올 때가 많습니다(특히 트리비얼 타입).
심화: 디버깅 가이드
| 증상 | 점검 |
|---|---|
| 복사가 막혀 있는데 이동도 안 됨 | 복사 delete 시 이동도 선언되지 않음 → 이동 = default 명시 |
Base*로 삭제 시 파생 소멸자 미호출 | 기본 클래스 소멸자가 non-virtual → virtual ~Base() = default |
| 파생 객체를 값으로 복사하면 파생 부분이 잘림(슬라이싱) | 다형 기반 클래스의 복사 연산 → protected로 두거나 = delete |
std::move했는데 여전히 느림 | 소멸자·복사 연산 선언으로 이동이 생성되지 않음 → 이동 = default 명시 |
vector에 넣을 수 없음 | 이동/복사 모두 불가능한 타입 → 저장 전략 재검토 |
심화: 흔한 실수 패턴 (추가)
- 기본 생성자만
private+= default인데 다른 생성자를 정의하지 않아 클래스 밖에서 생성 불가 — 의도한지 확인. operator=일부만delete: 나머지가 암시적으로 생성되어 의도와 다른 대입 가능 — Rule of Five 점검.- 소멸자만 정의하고 복사/이동을 안 씀: Rule of Three 시대 규칙과 충돌 — Rule of Five/Zero로 정리.
심화: 실전 예제 — 복사 금지·이동만 허용 (스레드 핸들 래퍼 개념)
class UniqueHandle {
void* h_{nullptr};
public:
explicit UniqueHandle(void* h) : h_(h) {}
~UniqueHandle() { /* close(h_) */ }
UniqueHandle(const UniqueHandle&) = delete;
UniqueHandle& operator=(const UniqueHandle&) = delete;
UniqueHandle(UniqueHandle&& o) noexcept : h_(o.h_) { o.h_ = nullptr; }
UniqueHandle& operator=(UniqueHandle&& o) noexcept {
if (this != &o) {
/* close(h_) */
h_ = o.h_;
o.h_ = nullptr;
}
return *this;
}
};
FAQ
Q1: default는 언제 사용?
A:
- 명시적 의도 표현
- 다른 생성자 정의 후 기본 생성자 필요
- trivial 타입 유지
Q2: delete는 언제 사용?
A:
- 복사/이동 금지
- 특정 타입 금지
- 힙 할당 금지
Q3: Rule of Five vs Rule of Zero?
A:
- Rule of Five: 리소스 직접 관리
- Rule of Zero: 스마트 포인터 사용 (권장)
Q4: 복사 금지 시 이동은?
A: 명시적으로 허용 필요. 자동 생성 안됩니다.
Q5: default의 장점?
A:
- 명확한 의도
- 컴파일러 최적화
- trivial 타입 유지
Q6: default/delete 학습 리소스는?
A:
- “Effective Modern C++”
- cppreference.com
- “C++ Primer”
같이 보면 좋은 글
- C++ explicit 키워드: 암시적 변환 막기, explicit operator bool, C++20 explicit(bool)
- C++ initializer_list 생성자: vector{10, 20}과 (10, 20)이 다른 이유와 중괄호 초기화 우선순위
- C++ explicit: Blocking Implicit Conversions, explicit operator bool, and C++20 explicit(bool)
- C++ 복사/이동 생성자