Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Changed the first NCCE/CS pair, added explicit references to destructors that can throw exceptions

...

  • When the exception handling mechanism, after completing the initialization of the exception object but before activation of a handler for the exception, calls a function that exits via an exception. ([except.throw], paragraph 7)
  • When a throw-expression with no operand attempts to rethrow an exception and no exception is being handled. ([except.throw], paragraph 9)
  • When the exception handling mechanism cannot find a handler for a thrown exception. ([except.handle], paragraph 9)
  • When the search for a handler encounters the outermost block of a function with a noexcept-specification that does not allow the exception. ([except.spec], paragraph 9)
  • When the destruction of an object during stack unwinding terminates by throwing an exception. ([except.ctor], paragraph 3)
  • When initialization of a non-local variable with static or thread storage duration exits via an exception. ([basic.start.init], paragraph 6)
  • When destruction of an object with static or thread storage duration exits via an exception. ([basic.start.term], paragraph 1)
  • When execution of a function registered with std::atexit() or std::at_quick_exit() exits via an exception. ([support.start.term], paragraphs 8 and 12)
  • When the implementation’s default unexpected exception handler is called. ([except.unexpected], paragraph 2) Note that std::unexpected() is currently deprecated.
  • When std::unexpected() throws an exception which is not allowed by the previously violated dynamic-exception-specification, and std::bad_exception() is not included in that dynamic-exception-specification. ([except.unexpected], paragraph 3)
  • When the function std::nested_exception::rethrow_nested() is called for an object that has captured no exception. ([except.nested], paragraph 4)
  • When execution of the initial function of a thread exits via an exception. ([thread.thread.constr], paragraph 5)
  • When the destructor is invoked on an object of type std::thread that refers to a joinable thread. ([thread.thread.destr], paragraph 1)
  • When the copy assignment operator is invoked on an object of type std::thread that refers to a joinable thread. ([thread.thread.assign], paragraph 1)
  • When calling condition_variable::wait()condition_variable::wait_until(), or condition_variable::wait_for() results in a failure to meet the post-condition: lock.owns_lock() == true or lock.mutex() is not locked by the calling thread. ([thread.condition.condvar], paragraphs 11, 16, 21, 28, 33, and 40)
  • When calling condition_variable_any::wait()condition_variable_any::wait_until(), or condition_variable_any::wait_for() results in a failure to meet the post-condition: lock is not locked by the calling thread. ([thread.condition.condvarany], paragraphs 11, 16, and 22)

...

In this noncompliant code example, the call to C::f() may result in thread entrypoint function thread_start() does not catch exceptions thrown by throwing_func(). If the initial thread function exits due to an exception being thrown. Since C::f() is called from a destructor, this can result in a call to std::terminate() if a non-local object of type C has static or thread storage duration, or if an object of type C is destroyed during stack unwinding (as in this example) is called.

Code Block
bgColor#FFcccc
langcpp
class C {
  void f() noexcept(false);
 
public:
  ~C() { f(); }
};
 
#include <thread>

void throwing_func() noexcept(false);
 
void f() noexcept(falsethread_start(void) {
  C c;
  throwing_func();
}
 
void gf() noexcept(true) {
  try {
    f(std::thread t(thread_start);
  } catch (...) {
    // Handle error
  }
}

If throwing_func() throws an exception, f() does not attempt to catch it and it will be handled by g(). However, during stack unwinding to reach the exception handler in g(), the automatic local variable c will be destroyed, resulting in a call to C::~C(). When the destructor attempts to throw an exception, std::terminate() will be called instead of throwing.

Note, the declaration for C::~C() does not comply with DCL40-CPP. Destructors and deallocation functions must be declared noexcept because destructors are implicitly declared noexcept(true), and this destructor allows exceptions by virtue of calling a function marked noexcept(false).

t.join();
}

Compliant Solution

In this compliant solution, the destructor for C handles thread_start() handles all exceptions and does not rethrow. When the automatic local variable c is destroyed, no exception is triggered from the C::~C() call, and the exception thrown by throwing_func() will be caught by the handler in g()., allowing the thread to terminate normally:

Code Block
bgColor#ccccff
langcpp
class C {
  void f#include <thread>

void throwing_func() noexcept(false);
 
public:
void  ~Cthread_start(void) {
    try {
      fthrowing_func();
    } catch (...) {
      // Handle error
    }
  }
};
 
void throwing_func() noexcept(false);
 
void f() noexcept(false) {
  C c;
  throwing_func();
}
 
void g() noexcept(true) {
  try {
    f();
  } catch (...) {
    // Handle error
  }std::thread t(thread_start);
  t.join();
}

Noncompliant Code Example

...