1. 배경

현재 진행하고 있는 C++ 프로젝트에서 custom allocator와 custom deleter를 사용하는 std::unique_ptr을 사용하고 싶었습니다. 그래서 custom allocator/deleter를 사용하는 UniquePtr타입과 UniquePtr을 생성하는 MakeUnique 함수를 만들었습니다. 그 코드는 아래와 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}

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. 개선

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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>());
}

type erasure 기법을 활용하여 UniquePtrDeleter의 객체 타입을 추상화 했습니다. 이 기법은 컴파일 타임에 결정되는 타입을 런타임에 추상화하기 위해 사용할 수 있습니다. 이것이 가능한 이유는, UniquePtrDeleter::Create<TObject>로 타입이 추상화된 UniquePtrDeleter를 생성하지만 내부적으로는 타입에 맞는 DeleterFunction에 대한 포인터를 들고 있기 때문입니다. Create는 함수 템플릿이기 때문에 컴파일 타임에 안전하게 타입에 맞는 DeleterFunction을 설정합니다. 약간의 trade-off가 존재 합니다. 런타임에 DeleterFunction에 대한 포인터를 유지해야 하기 때문에 std::unqieu_ptr의 크기가 늘어납니다. 64비트 환경이라면 객체에 대한 포인터 하나와 추가적으로 DeleterFunction에 대한 포인터를 갖게 되므로 16B 크기가 됩니다.