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 ofMoveAssignable
and assignment of the deleter from an rvalue of typeD
shall not throw an exception. Otherwise,D
is a reference type;remove_reference_t<D>
shall satisfy theCopyAssignable
requirements and assignment of the deleter from an lvalue of typeD
shall not throw an exception.(2) - Effects: Transfers ownership from
u
to*this
as if by callingreset(u.release())
followed byget_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:
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.
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.
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.
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)?
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.
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.
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.
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.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With