Search code examples
c++pointersshared-ptrsmart-pointers

What is the virtual function inside of shared_ptr's Control Block?


I've read an "Item" about shared_ptr in Scott Meyers' book "Effective Modern C++" where he says the following:

The usual control block implementation is more sophisticated than you might expect. It makes use of inheritance, and there’s even a virtual function. (It’s used to ensure that the pointed-to object is properly destroyed.) That means that using std::shared_ptrs also incurs the cost of the machinery for the virtual function used by the control block.

Then he doesn't explain what a virtual function does exactly. As far as I know, the proper way of deleting a pointed object is using deleters or type erasure. So, please explain what this is about.


Solution

  • The virtual function is required to ensure the object being shared is appropriately deleted. Unlike a unique_ptr, shared_ptr does not require full knowledge of the type when its template is instantiated. E.g. you can do this:

    class Foo;
    std::shared_ptr<Foo> foo = make_foo();
    

    Note that in the code above we don't have the complete Foo type, just the forward declaration. If we let foo go out of scope, the object it is pointing to will be correctly deleted because when Foo was created in make_foo, a deleter was also created which knows the complete type of Foo, and hence can call the appropriate destructors. (E.g. Perhaps make_foo creates a Bar that inherits from Foo and returns that. shared_ptr's will handle this fine.)

    The function on the deleter object that shared_ptr creates to manage the deletion of Foo will be virtual, allowing shared_ptr to call the correct destructor.

    Roughly this could be something like this:

    struct deleter_interface {
        virtual void ~deleter_interface = default;
        virtual void dispose() = 0;
    };
    
    template <typename T>
    struct deleter : deleter_interface {
        T* ptr_;
        deleter(T* ptr) : ptr_(ptr) {}
        virtual void dispose() { delete ptr_; }
    };
    
    template <typename T>
    shared_ptr {
        T* ptr_;
        deleter_interface* deleter_;
        ...
    };
    
    template <typename T>
    shared_ptr<T>::shared_ptr<T>(T* ptr)
        : ptr_(ptr), deleter_(new deleter<T>(ptr)) {}
    
    template <typename T>
    shared_ptr<T>::~shared_ptr<T>() { deleter_->dispose(); delete deleter_; }
    

    While this seems more complicated that is strictly necessary, it is to allow shared_ptr to be used without the complete type. For example what if you wanted to do this:

    In a.h:

    struct Foo;
    std::shared_ptr<Foo> a = make_object();
    // ... let a go out of scope
    

    And in a.cc:

    struct Foo { ... };
    struct Bar : Foo { ... };
    std::shared_ptr<Foo> make_object() { return std::shared_ptr<Foo>(new Bar); }
    

    If we didn't have the virtual function used in the deleter code, then Bar would not be destructed correctly. With the virtual function it doesn't matter that the header (a.h) never sees the definition of Foo or Bar.