gcc accepts the following program: struct S { S() = default; S(const S&) = delete; // int x = 0; // #3 }; int main() { try { throw S(); // #1 } catch (S s) { // #2 return 1; } } but it is ill-formed at #1 according to [except.throw] p5. Curiously, if line #3 is added, gcc flags an error at #2 (but not at #1).
Cross-reference: clang bug https://github.com/llvm/llvm-project/issues/57519
Confirmed.
The difference between #3 and not-#3 is that without the NSDMI, S isn't TYPE_NEEDS_CONSTRUCTING, which makes a difference in initialize_handler_parm: 339 /* If the constructor for the catch parm exits via an exception, we 340 must call terminate. See eh23.C. */ 341 if (TYPE_NEEDS_CONSTRUCTING (TREE_TYPE (decl))) 342 { 343 /* Generate the copy constructor call directly so we can wrap it. 344 See also expand_default_init. */ 345 init = ocp_convert (TREE_TYPE (decl), init, 346 CONV_IMPLICIT|CONV_FORCE_TEMP, 0, 347 tf_warning_or_error);
It's worse than this, "throw" will even move a non-movable value in the absence of a copy constructor: #include <iostream> #include <memory> using namespace std; int main(int argc, char const * const *argv) { unique_ptr<int> u(new int); cout << "u.get() first: " << (void*)u.get() << endl; try { throw u; } catch(...) { } cout << "u.get() after throw: " << (void*)u.get() << endl; }