Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++에서 shared_ptr을 활용한 가상 소멸 완벽 이해하기

개요

이 튜토리얼에서는 C++에서 shared_ptr을 사용한 가상 소멸(virtual destruction)이 어떻게 동작하는지 예제 프로그램을 통해 자세히 알아보겠습니다.

일반적으로 클래스의 인스턴스를 안전하게 삭제하려면 기본 클래스(base class)의 소멸자를 가상(virtual) 함수로 선언해야 합니다. 이렇게 하면 파생 클래스 객체가 삭제될 때 생성된 순서의 역순, 즉 파생 클래스 소멸자부터 기본 클래스 소멸자까지 차례대로 호출됩니다.

흥미로운 점은 std::shared_ptr의 경우입니다. shared_ptr은 객체를 생성하는 시점에 실제 타입 정보를 내부적으로 저장하기 때문에, 기본 클래스의 소멸자가 가상 함수가 아니더라도 올바른 소멸자를 호출해 줍니다. 아래 예제 코드로 직접 확인해 보겠습니다.

예제 코드

#include <iostream>
#include <memory>
using namespace std;
class Base {
    public:
    Base(){
        cout << "Constructing Base" << endl;
    }
    ~Base(){
        cout << "Destructing Base" << endl;
    }
};
class Derived : public Base {
    public:
    Derived(){
        cout << "Constructing Derived" << endl;
    }
    ~Derived(){
        cout << "Destructing Derived" << endl;
    }
};
int main(){
    std::shared_ptr<Base> sp{ new Derived };
    return 0;
}

실행 결과

Constructing Base
Constructing Derived
Destructing Derived
Destructing Base

결과 분석

실행 결과를 보면 객체 생성 시 Base → Derived 순서로 생성자가 호출되고, 프로그램이 종료되며 shared_ptr이 객체를 해제할 때는 반대로 Derived → Base 순서로 소멸자가 호출됩니다. 즉, 생성된 역순으로 모든 객체 인스턴스가 정확하게 삭제된 것을 확인할 수 있습니다.

만약 일반 포인터(Base* ptr = new Derived;)를 사용하고 delete를 호출했다면 어떻게 될까요? 기본 클래스의 소멸자가 가상 함수가 아니기 때문에 미정의 동작(undefined behavior)이 발생할 수 있으며, 대부분의 경우 파생 클래스의 소멸자가 호출되지 않아 리소스 누수로 이어집니다.

반면 shared_ptr은 객체를 가리키는 시점에 삭제자(deleter)에 실제 객체 타입 정보를 캡처하는 타입 소거(type erasure) 기법을 사용합니다. 덕분에 포인터 타입이 shared_ptr<Base>라 하더라도, 실제로 가리키는 객체가 Derived임을 기억하고 정확한 소멸자를 호출해 줍니다.

정리 및 주의 사항

shared_ptr이 가상 소멸자 없이도 올바른 소멸을 보장한다고 해서, 기본 클래스 소멸자를 가상으로 선언하지 않아도 되는 것은 아닙니다. 다음과 같은 이유로 다형적(polymorphic) 클래스를 설계할 때는 여전히 가상 소멸자 선언이 권장됩니다.

  • raw 포인터나 unique_ptr 등 다른 스마트 포인터로 객체를 관리할 경우 가상 소멸자가 없으면 미정의 동작이 발생할 수 있습니다.
  • 명시적인 가상 소멸자는 해당 클래스가 상속을 염두에 두고 설계되었음을 코드 수준에서 분명하게 드러냅니다.

결론적으로, shared_ptr은 타입 정보를 기억하여 안전한 소멸을 도와주는 편리한 도구이지만, 견고한 C++ 설계를 위해서는 기본 클래스에 가상 소멸자를 선언하는 습관을 유지하는 것이 가장 좋습니다.