C++ Rule of Zero : polymorphic deletion and unique_ptr behavior
Asked Answered
D

3

8

In the recent overload journal under the topic Enforcing the rule of zero, the authors describe how we can avoid writing the Rule of five operators as the reasons for writing them are:

  1. Resource management
  2. Polymorphic deletion

And both these can be taken care of by using smart pointers.

Here I am specifically interested in the second part.

Consider the following code snippet:

class Base
{
public:
    virtual void Fun() = 0;
};


class Derived : public Base
{
public:

    ~Derived()
    {
        cout << "Derived::~Derived\n";
    }

    void Fun()
    {
        cout << "Derived::Fun\n";
    }
};


int main()
{
    shared_ptr<Base> pB = make_shared<Derived>();
    pB->Fun();
}

In this case, as the authors of the article explain, we get polymorphic deletion by using a shared pointer, and this does work.

But if I replace the shared_ptr with a unique_ptr, I am no longer able to observe the polymorphic deletion.

Now my question is, why are these two behaviors different? Why does shared_ptr take care of polymorphic deletion while unique_ptr doesn't?

Dynameter answered 8/4, 2014 at 3:38 Comment(3)
how are you initializing the unique_pointer?Greg
Does it matter? Anyways like this : unique_ptr<Base> pB(new Derived())Dynameter
Because std::shared_ptr carries around a pointer to the deleter function. When you assign one std::shared_ptr to a compatible one the pointer is one of the members copied or moved. This does not happen with std::unique_ptr and since your base class that does not have a virtual destructor you get boned.Damiandamiani
S
4

You have your answer here: https://mcmap.net/q/464259/-virtual-destructor-with-virtual-members-in-c-11

Quote:

Once the last referring shared_ptr goes out of scope or is reset, ~Derived() will be called and the memory released. Therefore, you don't need to make ~Base() virtual. unique_ptr<Base> and make_unique<Derived> do not provide this feature, because they don't provide the mechanics of shared_ptr with respect to the deleter, because unique pointer is much simpler and aims for the lowest overhead and thus is not storing the extra function pointer needed for the deleter.

Shluh answered 8/4, 2014 at 5:3 Comment(1)
yet you can set up a different deleter type than the default, and it seems to read as if it will be copied around...Signally
G
3

It'll work if you use the C++14 make_unique or write your own one like in Yakk's answer. Basically the difference between the shared pointer behavior is that you got:

template<
    class T,
    class Deleter = std::default_delete<T>
> class unique_ptr;

for unique_pointer and as you can see, the deleter belongs to the type. If you declare a unique_pointer<Base> it'll always use std::default_delete<Base> as default. But make_unique will take care of using the correct deleter for your class.

When using shared_ptr you got:

template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );

and other overloads as constructor. As you can see the default deleter for unique_ptr depends on the template parameter when declaring the type (unless you use make_unique) whilst for shared_ptr the deleter depends on the type passed to the constructor.


You can see a version that allows polymorphic delete without virtual destructor here (this version should also work in VS2012). Note that it is quite a bit hacked together and I'm currently not sure what the behavior of unique_ptr and make_shared in C++14 will be like, but I hope they'll make this easier. Maybe I'll look into the papers for the C++14 additions and see if something changed if I got the time later.

Greg answered 8/4, 2014 at 4:10 Comment(2)
Does this mean that if I create the shared_ptr like this shared_ptr<Base> pB(new Derived()); I won't get polymorphic delete?Dynameter
I tried creating a shared_ptr as mentioned in my previous comment. But I still get polymorphic deletion(btw I am using VS2012 hope its not some VS specific non standard behavior)Dynameter
S
1
template<typename T>
using smart_unique_ptr=std::unique_ptr<T,void(*)(void*)>;

template<class T, class...Args> smart_unique_ptr<T> make_smart_unique(Args&&...args) {
  return {new T(std::forward<Args>(args)...), [](void*t){delete (T*)t;}};
}

The problem is that the default deleter for unique_ptr calls delete on the stored pointer. The above stores a deleter that knows the type at construction, so when copied to base class unique_ptr will still delete as the child.

This adds modest overhead, as we have to dereference a pointer. In addition it denormalizes the type, as default constructed smart_unique_ptrs are now illegal. You can fix this with some extra work (replace a raw function pointer with a semi smart functor that at least does not crash: the function pointer, however, should be asserted to exist if the unique is non-empty when the deleter is invoked).

Signally answered 8/4, 2014 at 3:55 Comment(6)
I used an implementation from this answer https://mcmap.net/q/67534/-make_unique-and-perfect-forwarding and still doesn't give me polymorphic deletionDynameter
@Dynameter Where is your virtual dtor?Apparatus
@Arun, why don't you have a virtual dtor in Base?Entangle
Because that is the point. I know I need a virtual destructor to get polymorphic destruction. But I am able to get away with it if I have a shared_ptr... then why not with a unique_ptr...thats my question!Dynameter
@arun I resd more, and figured out what goes wrong with default_delete. Code is first pass, but idea should work...Signally
bug: assumes U*->void*->T* is valid. It is not. Fixing is tricky.Signally

© 2022 - 2024 — McMap. All rights reserved.