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 byu
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:
unique_ptr::reset()
is called. unique_ptr
goes out of scope 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.