7

The behavior of std::unique_ptr with custom deleter is based on the static type of the deleter. No polymorphism, no runtime behavior based on actual deleter passed in runtime, as derived deleter provided is being sliced to the static type of the declared deleter.

(It is designed this way in purpose, to allow the size of unique_ptr with default deleter or with custom deleter without any data members, to have same size as a raw pointer).

static behavior of unique_ptr with custom deleter:

class A {};

struct BaseDeleter {
    virtual void operator()(A* p) const {
        std::cout << "in BaseDeleter" << std::endl; 
        delete p;
    }
};

struct DerivedDeleter: BaseDeleter {
    void operator()(A* p) const override {
        std::cout << "in DerivedDeleter" << std::endl; 
        delete p;
    }
};

int main() {
    auto unique_var = std::unique_ptr<A, BaseDeleter>(new A);
    unique_var = std::unique_ptr<A, DerivedDeleter>(new A);
}

Output:

in BaseDeleter
in BaseDeleter

This is opposed to std::shared_ptr who holds its custom deleter differently and allowing dynamic behavior:

dynamic behavior of shared_ptr with custom deleter:

int main() {
    auto shared_var = std::shared_ptr<A>(new A, BaseDeleter{});
    shared_var = std::shared_ptr<A>(new A, DerivedDeleter{});
}

Output:

in BaseDeleter
in DerivedDeleter

Code: https://coliru.stacked-crooked.com/a/54a8d2fc3c95d4c1


The behavior of assigning std::unique_ptr with different custom deleter is actually slicing.

Why unique_ptr doesn't prevent slicing of custom deleter?

Why didn't the language block the assignment of std::unique_ptr if the assigned unique_ptr has different custom deleter, to avoid slicing?


This seems to be possible as presented below.

Blocking unique_ptr from slicing of custom deleter

template<typename TYPE, typename Deleter>
struct my_unique_ptr : std::unique_ptr<TYPE, Deleter> {
    using BASE = std::unique_ptr<TYPE, Deleter>;
    using std::unique_ptr<TYPE, Deleter>::unique_ptr;
    auto& operator=(std::nullptr_t) noexcept {
        return BASE::operator=(nullptr);
    }
    template<typename T, typename OtherDeleter,
      std::enable_if_t<!std::is_same<OtherDeleter, Deleter>::value>* dummy = nullptr>
    auto& operator=(std::unique_ptr<T, OtherDeleter>&& other) = delete;
};

Code: http://coliru.stacked-crooked.com/a/089cd4c7303ad63e

Amir Kirsh
  • 12,564
  • 41
  • 74
  • My understanding - which may be incorrect - was that unique_ptr was optimized for its speed and memory footprint while shared_ptr was not. But perhaps I’m mistaken? – templatetypedef May 25 '19 at 20:15
  • 1
    @templatetypedef blocking the option of accidentally slicing your deleter can be done in compile time, as proposed in the question – Amir Kirsh May 25 '19 at 20:17
  • thanks for this question, i learned something new. lookinig forward to see the explanation. – skeller May 25 '19 at 20:21
  • 2
    Why block it? If the pointed to object can be polymorphically deleted, there is no issue. – StoryTeller - Unslander Monica May 25 '19 at 20:22
  • @StoryTeller see my example inside, it leads to slicing of the deleter, you would actually not get polymorphic behavior of the deleter. Which is probably never what you actually intended if provided an object with a derived deleter. Thus blocking should be right I believe. – Amir Kirsh May 25 '19 at 20:33
  • 1
    @AmirKirsh - It doesn't matter if we use the deleter of a base when the d'tor is virtual. That's what "polymorphic deletion" is. Not polymorphism of the deleter. Your idea will interfere with a basic use case. – StoryTeller - Unslander Monica May 25 '19 at 20:35
  • 1
    @StoryTeller can't see any logical valid use case that would become invalid with my suggestion. If you have one this would be a good answer to this question I guess. – Amir Kirsh May 25 '19 at 20:40
  • 2
    This looks more like a rant than a question, but anyway. With shared_ptr, the deleter resides with the **object** (in the control block). With unique_ptr, there is no control block, the deleter is stored **in the pointer itself** and is moved as the pointer gets passed around. – n. m. could be an AI May 25 '19 at 20:51
  • 1
    Blocking object slicing in this particular scenario doesn't seem any more justified than blocking it in any other scenario. I mean, slicing is bad, and eliminating it is a noble goal, but why start here? – n. m. could be an AI May 25 '19 at 20:54
  • @n.m. the different design of `unique_ptr` and `shared_ptr` as you rightfully present, may lead to undesired usage - as presented in the question. The bad usage could have been blocked by `unique_ptr`. Indeed there might be other bad scenarios elsewhere in the language (we are in C++ after all...). But I'm focusing on this one, which I believe could be improved, or maybe can still be improved without breaking any logical valid behavior. I think. – Amir Kirsh May 25 '19 at 21:22
  • 1
    Custom deleters that form a public-inheritance class hierarchy are a mistake in the first place. Why would anyone want such a thing? – n. m. could be an AI May 25 '19 at 21:26
  • 1
    Slicing is not always a bad thing, sometimes it is the correct thing. Also this way allows the deleters to be *convertible* between types - not everything is (or even should be) polymorphic. – Galik May 25 '19 at 21:45
  • Also, if for some reason you need polymorphic deleters, you could create a deleter wrapper that holds a reference to the deleter to circumvent undesirable slicing. – Galik May 25 '19 at 21:50
  • Note the deleter is either copy- or move- constructed from the given deleter. (In your specific case, it is indeed sliced - in `std::default_delete` it isn't, it just uses its converting ctor). The fact that it can be copy- or move-constructed can be useful - indeed it is for `std::default_delete`, whose `operator()` just `delete`s the pointed-to object, which is safe when the base and derived classes have a virtual dtor. One could imagine similar custom deleters, which safely polymorphically delete (using a `delete` expression or in some other manner), and perhaps perform some record keeping. – Danra Sep 26 '19 at 17:19
  • @Danra if you can present such a custom deleter case, where unique_ptr base type would reach the derived deleter provided this would be a good answer for my original question. I wasn't able to produce a _simple_ polymorphic custom deleter. I posted a code review for a not so simple polymorphic custom deleter: https://codereview.stackexchange.com/questions/229718/polymorphic-deleter-for-unique-ptr though I wonder if there is a simpler example. – Amir Kirsh Sep 26 '19 at 19:00

1 Answers1

3
struct B {
  virtual ~B() = default;
};

struct D : B {};

std::unique_ptr<B> b;
b = std::make_unique<D>();

Here we have a classic use case. Yes, the deleter is sliced, but deletion is still well defined. Your proposal will interfere with that. And will probably be very hard to amend reliably into not interfering.

One can always specify a custom deleter like std::function<void(void*)> to get polymorphism by type erasure. Sure, it has overhead, but that's opt in.

By default unique_ptr is optimized for the more common use cases, with less common ones that require overhead being possible by request.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Yet, slicing can be allowed for `default deleter` and blocked for `custom deleter`. [http://coliru.stacked-crooked.com/a/aa68c17f5ff06ac4](http://coliru.stacked-crooked.com/a/aa68c17f5ff06ac4) – Amir Kirsh May 25 '19 at 21:15
  • @AmirKirsh - Ah, so if I designed my own custom deleter that is well behaved with polymorphism I can't use it because "reasons"? UB is a staple of C++. It is not a safe language, and trying to make it one only ends up crippling your intended audience. – StoryTeller - Unslander Monica May 25 '19 at 21:21
  • I see no actual way to design a custom deleter that can be just sliced for another base deleter. Except for the default deleter which is a special case and can be handled separately. The cases where a user would slice a custom deleter would be a bug, always, I believe. Should the language stop the user from doing that? If it can I believe it should. – Amir Kirsh May 25 '19 at 21:27
  • @AmirKirsh - Well, then your stance is way more sure of itself compared to the committees. They tread far more carefully when blocking stuff, and still get a fair share of defect reports. The stance that slicing is always bad is way too extreme, in my not so humble opinion. – StoryTeller - Unslander Monica May 25 '19 at 22:16
  • Just for the record, the use case of different implementations for same type of custom deleter is supported in my proposed code: http://coliru.stacked-crooked.com/a/d4fd4c5e2536ee44 – Amir Kirsh Sep 26 '19 at 11:44