Search

C++ | type erasure 기법을 활용해 unique_ptr의 custom deleter 문제 해결

Date
2025/04/09
category
C++
Tags
c++

1. 배경

현재 진행하고 있는 C++ 프로젝트에서 custom allocator와 custom deleter를 사용하는 std::unique_ptr을 사용하고 싶었습니다. 그래서 custom allocator/deleter를 사용하는 UniquePtr타입과 UniquePtr을 생성하는 MakeUnique 함수를 만들었습니다. 그 코드는 아래와 같습니다.
template<typename TObject> struct UniquePtrDeleter { // 소멸자 호출 및 객체 해제 void operator()(TObject* pObject) const { pObject->~TObject(); SmartPointerAllocator<TObject>().deallocate(pObject, 1); } }; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr template<typename TObject> using UniquePtr = std::unique_ptr<TObject, UniquePtrDeleter<TObject>>; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr 생성 template<typename TObject, typename... Args> UniquePtr<TObject> MakeUnique(Args&&... args) { // 객체 할당 및 생성자 호출 TObject* pObject = SmartPointerAllocator<TObject>().allocate(1); new (pObject) TObject(std::forward<Args>(args)...); return UniquePtr<TObject>(pObject); }
C++
복사

2. 문제

처음엔 이렇게 코드를 작성해서 사용했지만 얼마 안 가 문제를 발견했습니다. 부모 클래스 타입 UniquePtr는 자식 클래스 타입 UniquePtr를 받을 수 없습니다. UniqePtr<Base> ptr = MakeUnique<Drived>(); 이런 코드가 불가능하다는 의미입니다. 이 코드가 불가능한 이유는, UniquePtrDeleter의 템플릿 파라미터에 전달되는 타입이 다르기 때문입니다. 전달되는 타입이 어떻게 다른지 std::unique_ptr로 풀어서 보면, std::unique_ptr<Base, UniquePtrDeleter<Base>> ptr = UniquePtr<Derived, UniquePtrDeleter<Derived>>; 이렇게 됩니다. 이 문제를 해결하기 위해선 UniquePtrDeleter를 객체 타입과 무관하게 사용할 수 있도록 객체 타입에 대한 의존성을 제거해야 합니다.

3. 개선

class UniquePtrDeleter { private: using DeleterFunction = void (*)(void*); public: UniquePtrDeleter() : _deleterFunction(DeleteNothing) {} explicit UniquePtrDeleter(DeleterFunction deleterFunction) : _deleterFunction(deleterFunction) {} // std::unique_ptr이 deleter를 호출할 때 이 메서드가 호출된다 void operator()(void* pObject) const { _deleterFunction(pObject); } // TObject 타입에 맞는 UniquePtrDeleter 생성 template<typename TObject> static UniquePtrDeleter Create() { return UniquePtrDeleter(DeleteObject<TObject>); } private: // 아무것도 하지 않는 Deleter static void DeleteNothing(void* pObject) noexcept {} // TObject 타입 객체 해제 작업을 수행 template<typename TObject> static void DeleteObject(void* pObject) noexcept { // 소멸자 호출 후 객체 해제 static_cast<TObject*>(pObject)->~TObject(); SmartPointerAllocator<TObject>().deallocate(static_cast<TObject*>(pObject), 1); } private: DeleterFunction _deleterFunction = nullptr; }; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr template<typename TObject> using UniquePtr = std::unique_ptr<TObject, UniquePtrDeleter>; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr 생성 template<typename TObject, typename... Args> UniquePtr<TObject> MakeUnique(Args&&... args) { // 메모리 할당 SmartPointerAllocator<TObject> allocator; TObject* pObject = allocator.allocate(1); try { // 생성자 호출 new (pObject) TObject(std::forward<Args>(args)...); } catch (...) { // 생성자 호출 중 예외 발생 시 메모리 해제 allocator.deallocate(pObject, 1); throw; } return UniquePtr<TObject>(pObject, UniquePtrDeleter::Create<TObject>()); }
C++
복사
type erasure 기법을 활용하여 UniquePtrDeleter의 객체 타입을 추상화 했습니다. 이 기법은 컴파일 타임에 결정되는 타입을 런타임에 추상화하기 위해 사용할 수 있습니다. 이것이 가능한 이유는, UniquePtrDeleter::Create<TObject>로 타입이 추상화된 UniquePtrDeleter를 생성하지만 내부적으로는 타입에 맞는 DeleterFunction에 대한 포인터를 들고 있기 때문입니다. Create는 함수 템플릿이기 때문에 컴파일 타임에 안전하게 타입에 맞는 DeleterFunction을 설정합니다. 약간의 trade-off가 존재 합니다. 런타임에 DeleterFunction에 대한 포인터를 유지해야 하기 때문에 std::unqieu_ptr의 크기가 늘어납니다. 64비트 환경이라면 객체에 대한 포인터 하나와 추가적으로 DeleterFunction에 대한 포인터를 갖게 되므로 16B 크기가 됩니다.