HOWTO · C++

C++에서 소멸자를 명시적으로 호출하는 방법

이 기사에서는 C++에서 소멸자를 명시적으로 호출하는 방법과 자동 정리가 이루어지는 시점, 객체의 수명이 끝난 뒤 해당 객체를 사용하면 안 되는 이유를 설명합니다.

이 페이지의 내용

C++에서는 obj.~ClassName()이라고 써서 객체의 소멸자를 명시적으로 호출합니다. 이 호출은 객체의 수명을 끝내므로, 그 저장 공간에 새 객체를 생성하지 않는 한 이후에는 해당 객체를 사용하면 안 됩니다. 소멸자를 직접 호출하는 경우는 드뭅니다. 일반적인 스코프 기반 정리나 std::unique_ptr 같은 소유권 타입을 사용하는 편이 대개 더 안전합니다.

obj.~ClassName() 표기법으로 소멸자 함수를 명시적으로 호출하기

소멸자는 객체의 수명이 끝날 때 그 객체가 소유한 리소스를 정리합니다. 자동 객체가 스코프를 벗어나면 컴파일러가 소멸자를 호출합니다. new가 반환한 포인터의 경우 delete가 소멸자를 호출한 다음 객체의 저장 공간을 해제합니다. 이러한 정상적인 정리 과정에 수동 호출을 더해서는 안 됩니다. 자동 객체의 소멸자를 직접 호출한 뒤 스코프를 벗어나게 두면, 이미 수명이 끝난 객체를 다시 소멸시키려는 상황이 됩니다. 이는 정의되지 않은 동작이며, 메시지 두 개가 안정적으로 출력되는 동작이 아닙니다. 마찬가지로 new로 만든 객체를 직접 소멸시킨 뒤 같은 포인터에 delete를 적용하는 것도 올바른 정리 순서가 아닙니다.

명시적 호출은 객체가 호출자가 제공한 저장 공간에 있고, 그 자리에 다른 객체를 생성할 때처럼 세심하게 관리하는 저수준 코드에서 유용합니다. 다음 예제는 바이트 버퍼에서 배치 new로 객체의 수명을 시작하고, 소멸자를 호출한 뒤 같은 주소에 대체 객체를 생성합니다. 마지막 명시적 호출은 대체 객체의 수명을 끝냅니다. 바이트 버퍼 자체는 그 안에 저장된 객체를 자동으로 소멸시키지 않습니다.

호출 형식은 객체 표현식 뒤에 .~와 클래스 이름을 붙이는 것입니다. Widget 클래스의 객체 이름이 obj라면 obj.~Widget()이라고 씁니다. 소멸자는 반환값이 없고 인수도 받지 않습니다. 접근 제한은 그대로 적용되므로, 접근 권한이 없는 코드에서는 private 소멸자를 호출할 수 없습니다. 소멸자 함수를 호출하면 정리 작업은 수행되지만, 할당자나 new로 얻은 저장 공간까지 해제되지는 않습니다. 저장 공간 관리는 별도로 처리해야 합니다.

예제에서 alignas(Widget)은 바이트 버퍼가 Widget에 필요한 정렬을 갖도록 하고, 버퍼 크기는 인스턴스 하나를 담기에 충분합니다. 배치 new는 별도 메모리를 요청하지 않고 이미 있는 버퍼에 객체를 생성합니다. 첫 번째 명시적 호출은 대체 객체를 생성하기 전에 첫 객체의 수명을 끝냅니다. 이어서 대체 객체도 명시적으로 소멸시킵니다. 저장 공간이 지역 바이트 배열이므로 스코프가 끝나면 배열의 수명만 끝납니다. 그 안에 배치된 Widget의 소멸자가 호출되지는 않습니다. 이 방식에서는 저장 공간에 현재 살아 있는 객체가 무엇인지 추적하고, 생성한 각 객체를 정확히 한 번 소멸시켜야 합니다.

일반적인 애플리케이션 코드에서는 지역 변수를 쓰는 편이 간단합니다. 함수가 반환되는 경우를 포함해 제어 흐름이 블록을 벗어나면 지역 변수의 소멸자가 실행됩니다. 동적 소유권이 필요하다면 std::unique_ptr<Widget>이 적합합니다. 소유 포인터의 수명이 끝날 때 객체도 함께 정리되기 때문입니다. 객체가 컨테이너나 다른 클래스의 구성 요소라면 구성 요소의 소멸자를 직접 호출하지 말고 해당 소유 객체가 정리하도록 두세요. 이런 방식은 프로그램 구조에서 정리 시점을 분명히 하고, 수명이 끝난 객체를 사용하는 일을 방지합니다.

다음 예제를 실행하세요:

#include <iostream>
#include <new>
#include <string>
#include <utility>

class Widget {
 public:
  explicit Widget(std::string name) : name_(std::move(name)) {
    std::cout << "created " << name_ << '\n';
  }

  ~Widget() { std::cout << "destroyed " << name_ << '\n'; }

 private:
  std::string name_;
};

int main() {
  alignas(Widget) unsigned char storage[sizeof(Widget)];
  Widget* obj = ::new (static_cast<void*>(storage)) Widget("first");
  obj->~Widget();
  obj = ::new (static_cast<void*>(storage)) Widget("second");
  obj->~Widget();
}
created first
destroyed first
created second
destroyed second

예제에 표시된 출력 레이블은 번역되지 않고 영어로 유지됩니다.

저장 공간과 수명을 직접 관리하지 않는 코드에서는 지역 객체의 스코프가 자연스럽게 끝나도록 하세요. 동적 소유권에는 std::unique_ptr을 우선 사용하세요. 소유 객체의 스코프가 끝날 때 소멸자가 delete를 사용해 객체를 정리합니다. 객체가 소멸된 뒤에는 해당 포인터에 접근하지 마세요. 구성 요소 리소스를 해제하려고 소멸자를 직접 호출하는 것도 피하고, 대신 적절한 표준 라이브러리 소유자나 리소스 래퍼를 사용하세요.