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

余生长醉 提交于 2019-12-08 16:37:58

问题


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)?


回答1:


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);



回答2:


Having reference data members often leads to surprising results because assigning to a reference has non-value semantics because a reference cannot be re-assigned to refer to another object. Basically, reference data members break your assignment operator semantics.

Using pointer members instead fixes that. Alternatively, use std::reference_wrapper<> and std::ref().


Why does it perform a deep copy of a deleter stored by a reference rather than just a shallow copy?

It performs member-wise copy. If the value being copied is a pointer, that happens to be a shallow copy.




回答3:


A reference can't be reseated after initialization. It acts in all ways as the object it refers to. And that includes assignment.

Because the reference acts as the object that it refers to, the copying of the referent is what you'd get in an ordinary class with an assignment operator implemented as a sequence of per-member assignments.



来源:https://stackoverflow.com/questions/35317344/unique-ptrs-assignment-operator-copies-a-deleter-stored-by-a-reference-is-it-a

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!