33

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 (i.e. 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)?

Niall
  • 30,036
  • 10
  • 99
  • 142
anderas
  • 5,744
  • 30
  • 49

4 Answers4

26

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.

Niall
  • 30,036
  • 10
  • 99
  • 142
  • 2
    But does standard require `T` to be Destructible? I can't find it anywhere? – el.pescado - нет войне Feb 19 '18 at 08:06
  • 2
    For the use of the default `Deleter`, it would. A custom one may be able to get around, but the call of the `Deleter` object would still need to not propagate any exceptions. – Niall Feb 19 '18 at 08:26
  • 1
    @el.pescado: Generally speaking, the standard library containers do not support types that have throwing dtors. If `unique_ptr::reset` can throw, then `unique_ptr::~unique_ptr` can throw, and then `std::vector` is disallowed by the standard, and similarly for all other containers. That's a pretty crappy situation, and there's really very little interest in using throwing dtors, so there's also little interest in supporting it within the standard library. – Chris Beck Feb 19 '18 at 18:29
  • 1
    This one is also good to cite, and more widely applicable: http://eel.is/c++draft/res.on.functions#2 – Deduplicator Feb 19 '18 at 21:27
  • @ChrisBeck that makes sense – el.pescado - нет войне Feb 20 '18 at 07:18
6

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.

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • `operator delete` just frees memory; `reset` must also invoke the destructor at some point – M.M Feb 19 '18 at 08:15
  • @M.M Do you mean calling delete operator as a function? Because delete operator invokes destructor. – user7860670 Feb 19 '18 at 08:24
  • @VTT I think it is defined in 15.4.12.4: "A destructor is also invoked implicitly through use of a delete-expression (8.5.2.5) for a constructed object allocated by a new-expression (8.5.2.4); the context of the invocation is the delete-expression." The delete operator does not invoke the destructor, it is implicitly invoked. – Jens Feb 19 '18 at 08:30
  • @Jens Yes, but there is no need to call destructor manually *"at some point"* after invoking delete operator. I think manual destructor calls typically happen only in some allocators that manage object and storage lifetimes separately. – user7860670 Feb 19 '18 at 08:36
  • @VTT I totally agree and was unsure when the destructor is actually called because the definition of `operator delete` does not invocation the invocation, that's why I looked it up. Personally, I think I used a manual destructor call only twice in my professional life, bot thimes when implementing some form of memory pool. – Jens Feb 19 '18 at 08:42
  • 3
    @VTT `operator delete` does not invoke the destructor. The `delete` expression invokes the destructor and then calls `operator delete` – M.M Feb 19 '18 at 09:15
  • This answer is wrong, for multiple reasons. #1: "`default_delete` is not declared as `noexcept` even though it only invokes `delete` operator which is required to be `noexcept`)." `default_delete` invokes the `delete-expression` (or `delete[]-expression` in the case of an array), not the delete operator. #3. The `delete-expression` is not and cannot be `noexcept` because some fool of a user might have a destructor that throws. #3: `std::default_delete::operator()` is not declared `noexcept`. ... – David Hammen Feb 19 '18 at 20:42
  • #4: The `delete` expression and `operator delete` are very different things. One calls the destructor but doesn't free memory. The other does just the opposite. #5: `std::unique_ptr` places a higher burden on the destructors of objects managed by `unique_ptr` and on the deleters used to delete them than do the less stringent but more generic requirements for `delete-expression`, `operator delete`, or `std::default_delete`. – David Hammen Feb 19 '18 at 20:44
  • @DavidHammen I think there is a strong misunderstanding. `delete p;` can be called *delete expression* **or** *delete operator invocation* (because `delete` is used here, well, as an operator). While `operator delete(p);` can be called *operator delete function invocation*. I did not refer to operator delete function call anywhere. – user7860670 Feb 19 '18 at 20:58
  • 1
    @VTT: The standard neither guarantees nor requires that `delete p;` does not throw an exception. It also neither guarantees nor requires that `operator delete(p)` does not throw an exception. It does however require that for a `std::unique_ptr`, that `get_deleter()(get())` be well-formed, have well-defined behavior, and be exception-free. The requirements for use in a `std::unique_ptr` are stronger than are the looser, more generic requirements. There's no contradiction here – David Hammen Feb 19 '18 at 21:16
  • Think of it in terms of the Liskov substitution principle. The base requirements are rather lax. About the only thing that `delete p` is guaranteed to do is to invoke `p`'s destructor. Making more strict requirements on what `delete` does / how it behaves in a specific circumstance is not a contradiction. Problems would arise only if requirements on what `delete` does / how it behaves were made less strict in a specific circumstance. – David Hammen Feb 19 '18 at 21:26
  • @DavidHammen Alright, `delete` function itself can be potentially throwing, since class may explicitly declare it as `noexcept(false)`. This is actually strange because behavior is undefined when deallocation function throws an exception. However I don't see how *shall not throw* can be a stronger requirement than `operator ()` of the deleter being `noexcept`. – user7860670 Feb 19 '18 at 21:56
  • @VTT - `std::default_delete::operator()` is not qualified as `noexcept`. – David Hammen Feb 19 '18 at 22:11
  • @DavidHammen That is exactly what I'm talking about. – user7860670 Feb 19 '18 at 22:13
4

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.

Community
  • 1
  • 1
martiert
  • 741
  • 4
  • 13
  • #5 doesn't happen: unwinding continues from the point _past_ the `unique_ptr` that threw. And if you find another throwing destructor, then you call `std::terminate`. – Davis Herring Feb 19 '18 at 18:00
0

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.

OLL
  • 655
  • 5
  • 20