Click here to Skip to main content
15,891,372 members
Articles / All Topics

shared_ptr Polymorphic Magic Pitfall

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
24 Apr 2014CPOL3 min read 9.8K   1  
shared_ptr Polymorphic Magic Pitfall


Pitfall in the polymorphic “magic” of std::shared_ptr

In [Sutter] we read:

“If A is intended to be used as a base class, and if callers should be able to destroy polymorphically, then make A::~A public and virtual. Otherwise make it protected (and not-virtual).”

Then in [ACCU], the author writes that if you use shared_ptr for polymorphic destruction, then you may omit the virtual destructor. The reason for this can be read from [Arena].

In short, through it’s template code, the shared_ptr remembers the pointer type used during construction. For example, if you say “shared_ptr<Base>{new Derived{}}” then shared_ptr will internally store a Derived*. If you say “shared_ptr<Base>{new Base{}}” then it stores a Base*. Then when the shared_ptr is destructed, it calls delete on the stored pointer. Naturally, with non-virtual destructors, for Base* it will call Base::~Base and for Derived* it will call Derived::~Derived.

If you are not careful and blindly follow the example in [ACCU], there is a nasty surprise waiting for you!
Let’s take the following example:

C++
#include <iostream>
#include <memory>

struct A
{
    ~A() { std::cout << __FUNCTION__ << std::endl; }
};

struct Base
{
    // No virtual dtor!
};

struct Derived : public Base
{
    Derived() : m_pA{new A{}} {}
    A m_A;
    std::unique_ptr<A> m_pA;
};

int main()
{
    {
        std::cout << "\nDelete Derived via Base* : A::~A() NOT CALLED" << std::endl;
        Base *p = new Derived{};
        delete p;
    }
    {
        std::cout << "\nDelete Derived via shared_ptr<Base> 
        with Derived* : A::~A() called" << std::endl;
        std::shared_ptr<Base> sp{new Derived{}};
    }
    {
        std::cout << "\nDelete Derived via make_shared<Derived> 
        : A::~A() called" << std::endl;
        std::shared_ptr<Base> sp{std::make_shared<Derived>()};
    }
    {
        std::cout << "\nDelete Derived via shared_ptr<Base> with Base* 
        : A::~A() NOT CALLED" << std::endl;
        Base *p = new Derived{};
        std::shared_ptr<Base> sp{p};
    }
    return 0;
}

Base does not have a virtual destructor, so if we delete a Derived pointer via Base then A::~A will not be called. Classic memory leak candidate.
The above code gives the following output:

Delete Derived via Base* : A::~A() NOT CALLED

Delete Derived via shared_ptr<Base> with Derived* : A::~A() called
~A
~A

Delete Derived via make_shared<Derived> : A::~A() called
~A
~A

Delete Derived via shared_ptr<Base> with Base* : A::~A() NOT CALLED

The first call is as expected, since Base has non-virtual destructor, A is not destructed. On the other hand, via the “magic” of shared_ptr, in the 2nd and 3rd cases, Derived::~Derived (and thus A::~A) gets called even though we have a shared_ptr<Base>. The 4th example has a surprise: although we use again a shared_ptr with a pointer to a Derived object, shared_ptr calls Base::~Base because it was initialized with a Base*. In this case, shared_ptr cannot see that the provided Base* pointer actually points to a Derived object.

shared_ptr<Base> calls Derived::~Derived only if it is constructed directly with a pointer of type Derived*. If you construct shared_ptr<Base> with a Base*, then it will not call Derived::~Derived, it will call ~Base::Base. The magic does not happen!

Also, in [Flaming] we read this:

“Classes that have custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership. Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators.”

But notice in our example above that neither Base nor Derived manage resources. Derived uses only self-destructing objects internally. So one might think that there is no ownership issue here, so we erroneously decide not to provide a “custom (virtual) destructor”. So the above should read like this:

“Classes that have custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership. Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators except for [Sutter].” :)

Conclusion

No matter how “smart” your pointer to Derived is and no matter if you use everywhere only self-destructing objects (e.g. RAII), use a virtual destructor for polymorphic deletion!

Again, as stated in [Sutter]:

“If A is intended to be used as a base class, and if callers should be able to destroy polymorphically, then make A::~A public and virtual. Otherwise make it protected (and not-virtual).”

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --