— 의도를 코드에 담는 방법
C++ 코드를 작성하다 보면 컴파일러가 자동 생성해주는 부분이 있어요.
대표적인 게 바로 기본 생성자, 복사 생성자, 소멸자 같은 특별 멤버 함수들이죠.
그럼 지금 그 코드 과연 내 의도가 잘 담긴 코드일까?
이 질문에서 출발해 default 와 delete 키워드를 깊게 알아봤어요.
default를 써야 했던 순간
처음에는 단순한 클래스였어요.
class MyObject {
public:
MyObject(int number) {
std::cout << "MyObject(int)" << std::endl;
}
};
이때 int 생성자를 하나 추가했는데, 갑자기 MyObject obj; 에서 오류가 났어요
“엥? 왜 안 되지?” 하다가 찾아보니,
사용자가 생성자를 하나라도 정의하면, 컴파일러는 기본 생성자를 만들어주지 않는다
라는 규칙 때문이었습니다.
그래서 다시 이렇게 수정했죠.
class MyObject {
public:
MyObject() = default; // 기본 생성자를 명시적으로 되살림
MyObject(int number) { std::cout << "MyObject(int)" << std::endl; }
};
그제서야 MyObject obj; 와 MyObject obj2(42); 가 제대로 동작했죠.
“아~ 그래서 = default가 필요했구나” 하고 깨달았습니다.
복사 생성자에 explicit을 붙인 이유
복사 생성자 역시 default를 붙여 선언해줘야 사용 가능하죠.
그런데 고민이 생겼습니다.
MyObject obj1(1);
MyObject obj2 = obj1; // 복사 생성자 호출
MyObject obj3 = 2; // 암시적 형변환 후 복사 생성자 호출 가능
문제는 이런 상황입니다. 함수 인자로 MyObject를 받을 때,
다른 타입이 우연히 MyObject로 변환되면서 복사가 발생할 수도 있다는 점이었죠.
그래서 저는 복사 생성자에 explicit 을 붙였습니다.
explicit MyObject(const MyObject&) = default;
이렇게 하면
- MyObject obj3(obj1); → ✅ 명시적 복사는 가능
- MyObject obj4 = obj1; → ❌ 암시적 변환은 차단
즉, 의도하지 않은 복사를 막을 수 있습니다.
virtual 소멸자가 필요한 이유
소멸자도 default로 선언해줘야 쓸 수 있었죠.
저는 이 클래스를 기반으로 파생 클래스를 만들 수 있다는 걸 염두에 두었어요. 그런데 만약 virtual을 빼먹으면?
MyObject* base = new DerivedObject();
delete base; // 기반 클래스 포인터로 삭제
이 경우 파생 클래스 소멸자가 호출되지 않아 자원 누수(memory leak) 가 생깁니다.
그래서 항상 습관처럼 이렇게 씁니다.
virtual ~MyObject() = default;
이러면 derived -> base 의 소멸자가 순차적으로 실행 되죠
delete로 원하지 않는 동작 막기
이제는 반대로 “이건 절대 되면 안 돼!” 싶은 경우가 있습니다.
예를 들어, 복사 금지 클래스를 만들고 싶을 때.
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 복사 금지
NonCopyable& operator=(const NonCopyable&) = delete; // 대입 금지
NonCopyable(NonCopyable&&) = default; // 이동 허용
NonCopyable& operator=(NonCopyable&&) = default;
};
또는 스택 전용/힙 전용 객체를 만들 때도 유용합니다.
class StackOnly {
public:
void* operator new(std::size_t) = delete; // 힙 할당 금지
};
class HeapOnly {
public:
~HeapOnly() = delete; // 스택에서 소멸 불가 → new로만 생성 가능
};
그리고 함수에서도 응용할 수 있습니다.
void acceptOnlyDouble(double) {}
template <typename T>
void acceptOnlyDouble(T) = delete;
acceptOnlyDouble(3.14); // ✅
acceptOnlyDouble(10); // ❌ 컴파일 에러
즉, delete는 “이건 하면 안 된다”라는 의도를 컴파일러에게 강제로 주입하는 방법입니다.
결론: 의도를 코드로 표현하자
정리하면:
- = default → “컴파일러 기본 구현을 쓰겠다”
- = delete → “이 동작은 막겠다”
- explicit → “암시적 변환을 허용하지 않겠다”
- virtual destructor → “상속 구조에서 안전하게 소멸시키겠다”
C++은 자유도가 높지만, 그만큼 위험한 함정도 많습니다.
저는 이 경험을 통해 “컴파일러에게 맡기지 말고, 내 의도를 코드로 정확히 표현하는 게 중요하다”는 걸 배웠어요.
'TIL > 2025' 카테고리의 다른 글
| Confluence 문서 상태 관리 feat. Comala (0) | 2025.11.07 |
|---|---|
| [cpp] type_traits (0) | 2025.09.24 |
| [cpp] Lambda Functions (0) | 2025.08.25 |
| for 조건문 내 후위 연산자의 위험성 (1) | 2025.08.15 |
| Guard Clause를 통해 바라본 Clean Code와 MISRA Rule의 차이 (3) | 2025.08.15 |