Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::unique_ptr reset() order of operations

Tags:

c++

unique-ptr

Calling void reset( pointer ptr = pointer() ) noexcept; invokes the following actions

Given current_ptr, the pointer that was managed by *this, performs the following actions, in this order:

  1. Saves a copy of the current pointer old_ptr = current_ptr
  2. Overwrites the current pointer with the argument current_ptr = ptr
  3. If the old pointer was non-empty, deletes the previously managed object if(old_ptr) get_deleter()(old_ptr).

cppreference

What is the reason for this particular order? Why not just do 3) and then 2)? In this question std::unique_ptr::reset checks for managed pointer nullity? the first answer quotes the Standard

[…] [ Note: The order of these operations is significant because the call to get_deleter() may destroy *this. —end note ]

Is this the only reason? How could get_deleter() destroy the unique_ptr (*this)?

like image 764
Ruperrrt Avatar asked Sep 01 '21 00:09

Ruperrrt


People also ask

What does unique_ptr Reset do?

std::unique_ptr::reset Destroys the object currently managed by the unique_ptr (if any) and takes ownership of p. If p is a null pointer (such as a default-initialized pointer), the unique_ptr becomes empty, managing no object after the call.

Does unique_ptr call Delete?

But do remember that unique_ptr are there so that you don't have to manage directly the memory they hold. That is, you should know that a unique_ptr will safely delete its underlying raw pointer once it goes out of scope.

What happens when you move a unique_ptr?

A unique_ptr can only be moved. This means that the ownership of the memory resource is transferred to another unique_ptr and the original unique_ptr no longer owns it.


Video Answer


1 Answers

It's often useful when analysing a prescribed order of steps, to consider which ones could throw and and what state that would leave everything in - with the intent that we should never end up in an unrecoverable situation.

Note that from the docs here that:

Unlike std::shared_ptr, std::unique_ptr may manage an object through any custom handle type that satisfies NullablePointer. This allows, for example, managing objects located in shared memory, by supplying a Deleter that defines typedef boost::offset_ptr pointer; or another fancy pointer.

So, in the current order:

  1. Saves a copy of the current pointer old_ptr = current_ptr

    if the copy constructor of fancy pointer throws, unique_ptr still owns the original object and new object is un-owned: OK

  2. Overwrites the current pointer with the argument current_ptr = ptr

    if the copy assignment of fancy pointer throws, unique_ptr still owns the original object and new object is un-owned: OK

    (this assumes the fancy pointer's copy assignment operator meets the usual exception-safety guarantee, but there's not much unique_ptr can do without that)

  3. If the old pointer was non-empty, deletes the previously managed object if(old_ptr) get_deleter()(old_ptr)

    at this stage unique_ptr owns the new object and it is safe to delete the old one.

In other words, the two possible outcomes are:

std::unique_ptr<T, FancyDeleter> p = original_value();
try {
  auto tmp = new_contents();
  p.reset(tmp);
  // success
}
catch (...) {
  // p is unchanged, and I'm responsible for cleaning up tmp
}

In your proposed order:

  1. If the original pointer is non-empty, delete it

    at this stage unique_ptr is invalid: it has committed the irreversible change (deletion) and there is no way to recover a good state if the next step fails

  2. Overwrite the current pointer with the argument current_ptr = ptr

    if the copy assignment of fancy pointer throws, our unique_ptr is rendered unusable: the stored pointer is indeterminate and we can't recover the old one

In other words, the unrecoverable situation I'm talking about is shown here:

std::unique_ptr<T, FancyDeleter> p = original_value();
try {
  auto tmp = new_contents();
  p.reset(tmp);
  // success
}
catch (...) {
  // I can clean up tmp, but can't do anything to fix p
}

After that exception, p can't even be safely destroyed, because the result of calling the deleter on its internal pointer might be a double-free.


NB. The deleter itself is not permitted to throw, so we don't have to worry about that.

The note saying

... the call to get_­deleter() might destroy *this.

sounds wrong, but the call get_­deleter()(old_­p) really might ... if *old_p is an object containing a unique_ptr to itself. The deleter call has to go last in that case, because there is literally nothing you can safely do to the unique_ptr instance after it.

Although this corner case is a solid reason for putting the deleter call at the end, I feel like the strong exception safety argument is perhaps less contrived (although whether objects with unique pointers to themselves are more or less common than fancy pointers with throwing assignment is anyone's guess).

like image 163
Useless Avatar answered Nov 12 '22 07:11

Useless