TIL/2025

[cpp] Request and Suppress (=default, =delete)

고무 오리 2025. 9. 8. 00:07
728x90

 

— 의도를 코드에 담는 방법

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++은 자유도가 높지만, 그만큼 위험한 함정도 많습니다.
저는 이 경험을 통해 “컴파일러에게 맡기지 말고, 내 의도를 코드로 정확히 표현하는 게 중요하다”는 걸 배웠어요.

 

728x90