简体   繁体   中英

Why is std::unique_ptr::reset() always noexcept?

A recent question (and especially my answer to it) made me wonder:

In C++11 (and newer standards), destructors are always implicitly noexcept , unless specified otherwise (ie noexcept(false) ). In that case, these destructors may legally throw exceptions. (Note that this is still a you should really know what you are doing -kind of situation!)

However, all overloads of std::unique_ptr<T>::reset() are declared to always be noexcept (see cppreference ), even if the destructor if T isn't, resulting in program termination if a destructor throws an exception during reset() . Similar things apply to std::shared_ptr<T>::reset() .

Why is reset() always noexcept, and not conditionally noexcept?

It should be possible to declare it noexcept(noexcept(std::declval<T>().~T())) which makes it noexcept exactly if the destructor of T is noexcept. Am I missing something here, or is this an oversight in the standard (since this is admittedly a highly academic situation)?

The requirements of the call to the function object Deleter are specific on this as listed in the requirements of the std::unique_ptr<T>::reset() member.

From [unique.ptr.single.modifiers]/3 , circa N4660 §23.11.1.2.5/3;

unique_ptr modifiers

void reset(pointer p = pointer()) noexcept;

Requires: The expression get_deleter()(get()) shall be well formed, shall have well-defined behavior, and shall not throw exceptions .

In general the type would need to be destructible. And as per the cppreference on the C++ concept Destructible , the standard lists this under the table in [utility.arg.requirements]/2 , §20.5.3.1 (emphasis mine);

Destructible requirements

u.~T() All resources owned by u are reclaimed, no exception is propagated .

Also note the general library requirements for replacement functions; [res.on.functions]/2 .

std::unique_ptr::reset does not invoke destructor directly, instead it invokes operator () of the deleter template parameter (which defaults to std::default_delete<T> ). This operator is required to not throw exceptions, as specified in

23.11.1.2.5 unique_ptr modifiers [unique.ptr.single.modifiers]

void reset(pointer p = pointer()) noexcept;

Requires: The expression get_deleter()(get()) shall be well-formed, shall have >well-defined behavior, and shall not throw exceptions.

Note that shall not throw is not the same as noexcept though. operator () of the default_delete is not declared as noexcept even though it only invokes delete operator (executes delete statement). So this seems to be a rather weak spot in the standard. reset should either be conditionally noexcept:

noexcept(noexcept(::std::declval<D>()(::std::declval<T*>())))

or operator () of the deleter should be required to be noexcept to give a stonger guarantee.

Without having been in the discussions in the standards committee, my first thought is that this is a case where the standards committee has decided that the pain of throwing in the destructor, which is generally considered undefined behaviour due to the destruction of stack memory when unwinding the stack, was not worth it.

For the unique_ptr in particular, consider what could happen if an object held by a unique_ptr throws in the destructor:

  1. The unique_ptr::reset() is called.
  2. The object inside is destroyed
  3. The destructor throws
  4. The stack starts unwinding
  5. The unique_ptr goes out of scope
  6. Goto 2

There was to ways of avoiding this. One is setting the pointer inside of the unique_ptr to a nullptr before deleting it, which would result in a memory leak, or to define what should happen if a destructor throws an exception in the general case.

Perhaps this would be easier to explain this with an example. If we assume that reset wasn't always noexcept , then we could write some code like this would cause problems:

class Foobar {
public:
  ~Foobar()
  {
    // Toggle between two different types of exceptions.
    static bool s = true;
    if(s) throw std::bad_exception();
    else  throw std::invalid_argument("s");
    s = !s;
  }
};

int doStuff() {
  Foobar* a = new Foobar(); // wants to throw bad_exception.
  Foobar* b = new Foobar(); // wants to throw invalid_argument.
  std::unique_ptr<Foobar> p;
  p.reset(a);
  p.reset(b);
}

What do we when p.reset(b) is called?

We want to avoid memory leaks, so p needs to claim ownership of b so that it can destroy the instance, but it also needs to destroy a which wants to throw an exception. So how and we destroy both a and b ?

Also, which exception should doStuff() throw? bad_exception or invalid_argument ?

Forcing reset to always be noexcept prevents these problems. But this sort of code would be rejected at compile-time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM