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?
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.