Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

unique_ptr's assignment operator copies a deleter stored by a reference. Is it a feature or a bug?

Imaging the case when you have an unique_ptr with a custom deleter stored by a reference:

struct CountingDeleter
{
    void operator()(std::string *p) {
        ++cntr_;
        delete p;
    }

    unsigned long cntr_ = 0;
};

int main()
{
    CountingDeleter d1{}, d2{};

    {
        std::unique_ptr<std::string, CountingDeleter&>
            p1(new std::string{"first"} , d1),
            p2(new std::string{"second"}, d2);

        p1 = std::move(p2); // does d1 = d2 under cover
    }

    std::cout << "d1 " << d1.cntr_ << "\n"; // output: d1 1
    std::cout << "d2 " << d2.cntr_ << "\n"; // output: d2 0
}

It was a surprise for me that the assignment in the code above has a side-effect of copying d2 into d1. I've double check it and found that this behavior is as described in the standard in [unique.ptr.single.asgn]:

(1) - Requires: If D is not a reference type, D shall satisfy the requirements of MoveAssignable and assignment of the deleter from an rvalue of type D shall not throw an exception. Otherwise, D is a reference type; remove_reference_t<D> shall satisfy the CopyAssignable requirements and assignment of the deleter from an lvalue of type D shall not throw an exception.

(2) - Effects: Transfers ownership from u to *this as if by calling reset(u.release()) followed by get_deleter() = std::forward<D>(u.get_deleter()).

To get the behavior that I expected (a shallow copy of the deleter reference) I had to wrap the deleter reference into std::reference_wrapper:

std::unique_ptr<std::string, std::reference_wrapper<CountingDeleter>>
    p1(new std::string{"first"} , d1),
    p2(new std::string{"second"}, d2);

p1 = std::move(p2); // p1 now stores reference to d2 => no side effects!

For me the current handling of a deleter reference in the unique ptr is counter-intuitive and even error-prone:

  1. When you store a deleter by a reference rather than by value this mostly because you want the shared deleter with some important unique state. So you don't expect the shared deleter is overwritten and its state is lost after a unique ptr assignment.

  2. It's expected that assignment of a unique_ptr is extremely chip, especially if the deleter is a reference. But instead of this, you get copying of the deleter what can be (unexpectedly) expensive.

  3. After the assignment, the pointer become bound to original deleter's copy, rather than to the original deleter itself. This might lead to some unexpected side-effects if the deleter's identity is important.

  4. Also, current behavior prevents from using a const reference to a deleter because you just can't copy into a const object.

IMO it would be better to forbid a deleters of reference types and accept only a movable value types.

So my question is the following (it looks like two questions in one, sorry):

  • Is there any reason why the standard unique_ptr behaves like this?

  • Does anybody have a good example where it's useful to have a reference type deleter in unique_ptr rather than a non-reference one (i.e. a value type)?

like image 249
oliora Avatar asked Feb 10 '16 14:02

oliora


People also ask

Does unique_ ptr delete automatically?

unique_ptr objects automatically delete the object they manage (using a deleter) as soon as they themselves are destroyed, or as soon as their value changes either by an assignment operation or by an explicit call to unique_ptr::reset.

What does unique_ ptr do in c++?

std::unique_ptr is a smart pointer that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope. The object is disposed of, using the associated deleter when either of the following happens: the managing unique_ptr object is destroyed.

How to transfer ownership unique_ ptr?

In C++11 we can transfer the ownership of an object to another unique_ptr using std::move() . After the ownership transfer, the smart pointer that ceded the ownership becomes null and get() returns nullptr.

How does unique_ ptr work?

A unique_ptr object wraps around a raw pointer and its responsible for its lifetime. When this object is destructed then in its destructor it deletes the associated raw pointer. unique_ptr has its -> and * operator overloaded, so it can be used similar to normal pointer.


1 Answers

This is a feature.

If you have stateful deleters presumably the state is important, and is associated with the pointer that it will be used to delete. That means the deleter state should be transferred when ownership of the pointer transfers.

But if you store a deleter by reference it means you care about the identity of the deleter, not just its value (i.e. it's state), and updating the unique_ptr should not re-bind the reference to a different object.

So if you don't want this, why are you even storing a deleter by reference?

What does a shallow copy of a reference even mean? There's no such thing in C++. If you don't want reference semantics, don't use references.

If you really want to do this, then the solution is simple: define assignment for your deleter to not change the counter:

CountingDeleter&
operator=(const CountingDeleter&) noexcept
{ return *this; }

Or since what you really seem to care about is the counter, not the deleter, keep the counter outside the deleter and don't use reference deleters:

struct CountingDeleter
{
    void operator()(std::string *p) {
        ++*cntr_;
        delete p;
    }

    unsigned long* cntr_;
};

unsigned long c1 = 0, c2 = 0;
CountingDeleter d1{&c1}, d2{&c2};

{
    std::unique_ptr<std::string, CountingDeleter>
        p1(new std::string{"first"} , d1),
        p2(new std::string{"second"}, d2);
like image 153
Jonathan Wakely Avatar answered Oct 19 '22 21:10

Jonathan Wakely