Question

I was reading the latest Overload (link) and decided to test out the statement at page 8:

shared_ptr will properly invoke B’s destructor on scope exit, even though the destructor of A is not virtual.

I am using Visual Studio 2013, compiler v120:

#include <memory>
#include <iostream>

struct A {
    ~A() { std::cout << "Deleting A"; }
};


struct B : public A
{
    ~B() { std::cout << "Deleting B"; }
};

int main()
{
    std::shared_ptr<A> ptr = std::make_shared<B>();
    ptr.reset();

    return 0;
}

This works as expected and prints out "Deleting BDeleting A"

The article seems to imply that this should also work with std::unique_ptr:

There is almost no need to manage your own resources so resist the temptation to implement your own copy/assign/move construct/move assign/destructor functions.

Managed resources can be resources inside your class definition or instances of your classes themselves. Refactoring the code around standard containers and class templates like unique_ptr or shared_ptr will make your code more readable and maintainable.

However, when changing

    std::shared_ptr<A> ptr = std::make_shared<B>();

to

    std::unique_ptr<A> ptr = std::make_unique<B>();

the program will only output "Deleting A"

Did I misunderstand the article and the behavior is intended by the standard? Is it a bug with the MSVC compiler?

Was it helpful?

Solution

shared_ptr and unique_ptr are different. make_shared will create a type erased deleter object on invocation while with unique_ptr the deleter is part of the type. Therefore the shared_ptr knows the real type when it invokes the deleter but the unique_ptr doesn't. This makes unique_ptr's much more efficient which is why it was implemented this way.

I find the article a bit misleading actually. I do not consider it to be good advice to expose copy constructors of a base class with virtual functions, sounds like a lot of slicing problems to me.

Consider the following situation:

struct A{
    virtual void foo(){ std::cout << "base"; };
};
struct B : A{
    virtual void foo(){ std::cout << "derived"; };
};
void bar(A& a){
    a.foo(); //derived
    auto localA = a; //poor matanance porgrammer didn't notice that a is polymorphic
    localA.foo(); //base
}

I would personally advocate non-intrusive polymorphism http://isocpp.org/blog/2012/12/value-semantics-and-concepts-based-polymorphism-sean-parent for any new higherarchies, it sidesteps the problem entirely.

OTHER TIPS

This behaviour is according to the standard.

The unique_ptr is designed to have no performance overhead compared to the ordinary new/delete.

The shared_ptr has the allowance to have the overhead, so it can be smarter.

According to the standard [20.7.1.2.2, 20.7.2.2.2], the unique_ptr calls delete on the pointer returned by get(), while the shared_ptr deletes the real object it holds - it remembers the proper type to delete (if properly initialized), even without a virtual destructor.

Obviously, the shared_ptr is not all-knowing, you can trick it to behave badly by passing a pointer to the base object like this:

std::shared_ptr<Base> c = std::shared_ptr<Base> { (Base*) new Child() };

But, that would be a silly thing to do anyhow.

You could do something similar with unique_ptr, but since its deleter type is determined statically, you need to statically maintain the proper deleter type. i.e. (Live demo at Coliru):

// Convert given pointer type to T and delete.
template <typename T>
struct deleter {
    template <typename U>
    void operator () (U* ptr) const {
        if (ptr) {
            delete static_cast<T*>(ptr);
        }
    }
};

// Create a unique_ptr with statically encoded deleter type.
template <typename T, typename...Args>
inline std::unique_ptr<T, deleter<T>>
make_unique_with_deleter(Args&&...args) {
    return std::unique_ptr<T, deleter<T>>{
        new T(std::forward<Args>(args)...)
    };
}

// Convert a unique_ptr with statically encoded deleter to
// a pointer to different type while maintaining the
// statically encoded deleter.
template <typename T, typename U, typename W>
inline std::unique_ptr<T, deleter<W>>
unique_with_deleter_cast(std::unique_ptr<U, deleter<W>> ptr) {
    T* t_ptr{ptr.release()};
    return std::unique_ptr<T, deleter<W>>{t_ptr};
}

// Create a unique_ptr to T with statically encoded
// deleter for type U.
template <typename T, typename U, typename...Args>
inline std::unique_ptr<T, deleter<U>>
make_unique_with_deleter(Args&&...args) {
    return unique_with_deleter_cast<T>(
        make_unique_with_deleter<U>(std::forward<Args>(args)...)
    );
}

It's a bit awkward to use:

std::unique_ptr<A, deleter<B>> foo = make_unique_with_deleter<A, B>();

auto improves the situation:

auto bar = make_unique_with_deleter<A, B>();

It doesn't really buy you much, since the dynamic type is encoded right there in the static type of the unique_ptr. If you're going to carry the dynamic type around, why not simply use unique_ptr<dynamic_type>? I conjecture such a thing may have some use in generic code, but finding an example of such is left as an exercise to the reader.

The technique std::shared_ptr employs to do the magic is called type erasure. If you are using gcc, try to find the file bits/shared_ptr_base.h and check the implementation. I'm using gcc 4.7.2.

unique_ptr is designed for minimum overhead and does not use type erasure to remember the actual type of the pointer it holds.

Here is a great discussion on this topic: link


EDIT: a simple implementation of shared_ptr to showcase how type erasure is achieved.

#include <cstddef>

// A base class which does not know the type of the pointer tracking
class ref_counter_base
{
  public:
    ref_counter_base() : counter_(1) {}
    virtual ~ref_counter_base() {}

    void increase()
    {
      ++counter_;
    }
    void release() 
    {
      if (--counter_ == 0) {
        destroy();
        delete this;
      }
    }

    virtual void destroy() = 0;
  private:
    std::size_t counter_;
};

// The derived class that actually remembers the type of
// the pointer and deletes the pointer on destroy.
template <typename T>
class ref_counter : public ref_counter_base
{
  public:
    ref_counter(T *p) : p_(p) {}
    virtual void destroy() 
    {
      delete p_;
    }
  private:
    T *p_;
};

template <typename T>
class shared_ptr
{
  public:
    shared_ptr(T *p) 
      : p_(p)
      , counter_(new ref_counter<T>(p))
    {
    }

    // Y* should be implicitely convertable to T*,
    // i.e. Y is derived from T
    template <typename Y>
      shared_ptr(Y &other)
      : p_(other.get())
      , counter_(other.counter())
      {
        counter_->increase();
      }

    ~shared_ptr() 
    {
      counter_->release();
    }

    T* get() { return p_; }
    ref_counter_base* counter() { return counter_; }
  private:
    T *p_;
    ref_counter_base *counter_;
};
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top