C++ = default와 = delete: 특수 멤버 함수 제어와 복사 금지·이동 전용 타입
이 글의 핵심
복사 생성자를 delete했더니 이동도 함께 사라지는 문제처럼, 특수 멤버 함수는 하나를 선언하면 다른 함수의 암시적 생성 여부가 바뀝니다. = default가 noexcept와 트리비얼 타입 여부에 주는 영향, 추상 클래스에서 가상 소멸자를 default로 두는 방법, 모호한 오버로드를 delete로 막는 패턴을 설명합니다.
default와 delete란?
= default와 = delete는 C++11에서 도입된 문법으로, 특수 멤버 함수를 컴파일러의 암시적 규칙에 맡기지 않고 명시적으로 만들거나 없앨 때 씁니다.
그림: 특수 멤버 함수 하나를 직접 선언하면 나머지 자동 생성이 조용히 바뀐다. 생성자를 만들면 기본 생성자가 사라지고, 소멸자나 복사 생성자를 선언하면 이동 요청이 복사로 대체되며, 이동 생성자를 선언하면 복사가 삭제된다(이 맥의 clang++로 확인).
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”라는 보충 설명(Clang의 경우 ”… is implicitly deleted because field … 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;를 추가해야 합니다.
그림: 실제 실행 결과 — 소멸자만 직접 선언해도 복사 생성자는 계속 만들어지고(이동 요청은 복사로 대체된다), unique_ptr 멤버가 있으면 복사는 자동으로 삭제되고 이동만 가능하다. (환경: macOS 27.0.1 · Apple clang 21.0.0)
문제 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로 되살려야 합니다.
일반 함수의 특정 오버로드 금지
= delete는 특수 멤버 함수뿐 아니라 일반 함수에도 쓸 수 있습니다. 예시 3의 생성자와 같은 원리로, 원치 않는 인자 타입이 정확히 일치하는 오버로드를 삭제해 두면 암시적 변환으로 다른 오버로드가 호출되는 것을 막을 수 있습니다.
#include <iostream>
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된 함수
}
템플릿 특수화 금지
#include <iostream>
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된 특수화
}
함수 템플릿의 명시적 특수화도 삭제할 수 있습니다. 다만 막히는 것은 T가 정확히 double로 추론되는 경우뿐이라, process(3.14f)는 T = float로 추론되어 그대로 컴파일됩니다. 여러 타입을 한꺼번에 막아야 한다면 C++20 concepts나 static_assert로 템플릿 자체에 제약을 거는 편이 낫습니다.
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) {}
};
두 클래스는 기본 생성 결과가 같습니다. 차이는 유지보수에서 납니다. PointA는 초기값이 멤버 선언 한 곳에 있으므로 나중에 다른 생성자를 추가해도 그 생성자가 초기화하지 않은 멤버는 자동으로 0이 됩니다. PointB 방식은 생성자마다 초기화 리스트를 반복해야 하고, 하나를 빠뜨리면 그 생성자에서만 멤버가 초기화되지 않습니다. 특별한 로직 없이 멤버를 초기화하기만 한다면 A안이 관리하기 쉽습니다.
심화: = default와 noexcept·트리비얼 타입
이동 연산이 noexcept가 아니면 표준 컨테이너가 이동 대신 복사를 선택해 비용이 커질 수 있습니다. std::vector는 재할당할 때 강한 예외 보장을 지키기 위해 std::move_if_noexcept를 쓰는데, 이동 생성자가 예외를 던질 수 있다고 선언되어 있으면(그리고 복사가 가능하면) 이동 대신 복사를 합니다.
사실 = default로 만든 이동 연산은 모든 멤버의 이동이 noexcept라면 자동으로 noexcept가 됩니다. 그래도 명시적으로 적는 이유는 두 가지입니다. 하나는 의도를 문서화하는 것이고, 다른 하나는 나중에 noexcept가 아닌 멤버가 추가됐을 때 이를 드러내는 것입니다. C++20부터는 명시한 noexcept가 암시적 명세와 달라도 적은 대로 적용되므로, 실제로는 예외를 던질 수 있는 멤버가 있는데 noexcept로 선언하면 예외가 나는 순간 std::terminate로 프로그램이 종료된다는 점도 기억해야 합니다. static_assert(std::is_nothrow_move_constructible_v<Buffer>);를 한 줄 넣어 두면 이런 변화를 컴파일 타임에 잡을 수 있습니다.
struct Buffer {
Buffer() = default;
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
};
트리비얼 여부도 = default를 쓰는 위치에 따라 달라집니다. 첫 선언에서 = default로 둔 복사 생성자는 멤버가 모두 트리비얼하면 트리비얼하게 남으므로, 그 타입은 std::is_trivially_copyable을 만족하고 표준 라이브러리 구현이 std::copy나 vector 재할당을 memmove로 처리할 수 있습니다. 같은 동작을 Point(const Point& o) : x(o.x), y(o.y) {}처럼 직접 작성하면 사용자 제공 함수가 되어 이 성질을 잃습니다.
심화: 인터페이스에서 = default (추상 클래스)
순수 가상 함수만 있는 클래스라도 가상 소멸자는 = default로 둘 수 있습니다.
struct IPlugin {
virtual ~IPlugin() = default;
virtual void run() = 0;
};
이 소멸자도 사용자 선언 소멸자이므로 IPlugin의 이동 연산은 암시적으로 선언되지 않습니다. 인터페이스 타입은 대개 포인터로만 다루기 때문에 문제가 되지 않지만, 파생 클래스가 이동 가능해야 한다면 파생 클래스 쪽 멤버 구성을 확인하세요. 기반의 복사는 여전히 (폐지 예정 규칙에 따라) 생성되므로 파생 클래스의 이동은 기반 부분을 복사로 처리합니다.
심화: = delete로 모호한 오버로드·변환 차단
struct Index {
explicit Index(int v) : v_(v) {}
Index(double) = delete; // Index(3.7)처럼 double이 int로 잘리는 실수 방지
private:
int v_{};
};
explicit은 암시적 변환을 막을 뿐, Index(3.7)처럼 직접 초기화할 때 double이 int로 잘리는 것은 막지 못합니다. 삭제된 double 생성자가 정확히 일치하는 후보로 선택되어 에러를 내므로 두 장치를 함께 쓰는 경우가 많습니다.
delete된 함수는 본문이 없으므로 바이너리에 코드가 들어가지 않습니다. 삭제는 컴파일 타임에 호출을 막는 장치일 뿐 런타임 비용이 없습니다.
심화: 디버깅 가이드
| 증상 | 점검 |
|---|---|
| 복사가 막혀 있는데 이동도 안 됨 | 복사 연산을 선언하면 이동이 선언되지 않음 → 이동 연산을 = default로 명시 |
Base*로 삭제 시 파생 소멸자 미호출 | 기반 클래스 소멸자가 non-virtual → virtual ~Base() = default; |
| 파생 객체를 값으로 복사하면 파생 부분이 잘림(슬라이싱) | 다형 기반 클래스의 복사 연산을 protected로 두거나 = delete |
std::move했는데 여전히 느림 | 소멸자·복사 연산 선언으로 이동이 생성되지 않음 → 이동 연산을 = default로 명시 |
vector에 넣을 수 없음 | 이동과 복사가 모두 불가능한 타입 → unique_ptr로 감싸거나 emplace_back과 재할당 없는 컨테이너(std::deque, std::list) 검토 |