Search code examples
c++shared-ptrdestructorunique-ptr

Creating unique_ptr<Base> when Base class has a protected destructor


class Base {
public:
    Base() {}
    virtual void print()const = 0;
protected:
    virtual ~Base() { std::cout << "Base destructor\n\n"; }
};

int main()
{
    //std::vector<std::unique_ptr<Base>> v1; 
    //The line above won't compile because: 'Base::~Base': cannot access protected member declared in class 'Base'
    std::vector<std::shared_ptr<Base>> v2;
    return 0;
}

What is trying to call the destructor when I am creating the vector? Why it won't compile for the unique_ptr vector but would compile for the shared_ptr vector?


Solution

  • Local variables v1 and v2 have automatic storage duration and will be automatically destructed when they go out of scope. The std::vector is irrelevant here: inside vector::~vector() the compiler will generate code for element destructors. Even if a vector is always empty (this is a run-time property!), this code still has to be generated. So let's simplify the code:

    std::unique_ptr<Base> v1;
    std::shared_ptr<Base> v2;
    

    When v1 goes out of scope, it has to be destroyed. The destructor generated by the compiler boils down to (*):

    ~unique_ptr() {
        delete ptr;
    }
    

    To generate code for delete ptr, the compiler needs the accessible destructor. It is protected, so the compilation fails.

    Now let's look at v2. The compiler has to generate the destructor, too. But shared_ptr has a type-erased deleter for a managed object. This means that a managed object destructor will be called indirectly – through a virtual function:

    struct shared_ptr_deleter_base {
        virtual void destroy() = 0;
        virtual ~shared_ptr_deleter_base() = default;
    };
    
    ~shared_ptr() {
        // member shared_ptr::deleter has type shared_ptr_deleter_base*
        if (deleter)
            deleter->destroy();
    }
    

    To generate code for deleter->destroy(), you don't need to access Base::~Base() at all. The default constructor of shared_ptr simply sets deleter to a null pointer:

    shared_ptr() {
        deleter = nullptr;
    }
    

    That's why std::shared_ptr<Base> v2; compiles: not only Base::~Base() is not called at the run-time, no call is ever generated by the compiler at the compile-time.

    Let's consider this line:

    std::shared_ptr<Base> v2(new Base());
    

    Now the following constructor is called (note that it is a template with a separate parameter U that can be different from T in shared_ptr<T>):

    template<class U>
    shared_ptr(U* ptr) {
        deleter = new shared_ptr_deleter<U>(ptr);
    }
    

    Here shared_ptr_deleter is a concrete class derived from shared_ptr_deleter_base:

    template<class T>
    struct shared_ptr_deleter : shared_ptr_deleter_base {
        T* ptr;
        shared_ptr_deleter(T* p) : ptr(p) {}
    
        virtual void destroy() {
            delete ptr;
        }
    };
    

    To generate code for the constructor taking new Base(), the compiler has to generate code for shared_ptr_deleter<Base>::destroy(). Now it fails because the Base::~Base() is inaccessible.

    (*) I present only simplified definitions just to demonstrate basic ideas without going into all the details that are not relevant to understanding the problem in question.