13

From other threads, I know we should not throw exception in destructor! But for below example, it really works. Does that means we can only throw exception in one instance's destructor? how should we understand this code sample!

#include <iostream>
using namespace std;
class A {
 public:
  ~A() {
    try {
      printf("exception in A start\n");
      throw 30;
      printf("exception in A end\n");      
    }catch(int e) {
      printf("catch in A %d\n",e);
    }
  }
};
class B{
 public:
  ~B() {
    printf("exception in B start\n");
    throw 20;
    printf("exception in B end\n");    
  }
};
int main(void) {
  try {
    A a;
    B b;
  }catch(int e) {
    printf("catch in main %d\n",e);
  }
  return 0;
}

The output is:

exception in B start
exception in A start
catch in A 30
catch in main 20
beetlej
  • 1,841
  • 4
  • 13
  • 27
  • 8
    Try sticking a `throw 42;` after your `B b;`. – Fred Larson Dec 15 '16 at 22:17
  • 2
    Actually, what you have didn't work for me. I got `terminate called after throwing an instance of 'int'`. – Fred Larson Dec 15 '16 at 22:20
  • 6
    *But for below example, it really works* -- I wish that persons would stop trying to prove that undefined behavior doesn't exist if they write code a certain way. – PaulMcKenzie Dec 15 '16 at 22:20
  • 1
    @PaulMcKenzie: is it really undefined behavior? AFAICR it's well defined that letting an exception escape a destructor should call `std::terminate`, if it doesn't the implementation is probably buggy. – Matteo Italia Dec 15 '16 at 22:25
  • 1
    Undefined behaviour includes appearing to work perfectly. Until it fails , in production, at midnight, on xmas day, for your biggest customer – pm100 Dec 15 '16 at 22:26
  • 1
    What compiler are you using? If it's VC++, did you use the /EHsc command line switch? Because without it the default behavior for exception handling is completely broken. – Matteo Italia Dec 15 '16 at 22:27
  • @MatteoItalia - Well, yes it should call `std::terminate`. However my main point is that if you've read from experienced programmers that "doing x causes undefined behavior" or "doing x throws an exception", and you waste time trying to write code to counter this, doesn't it seem to be a waste of time? A good percentage of questions on SO follow this path to nowhere. – PaulMcKenzie Dec 15 '16 at 22:27
  • With what compiler did you get that output? – Ben Voigt Dec 15 '16 at 22:32
  • This is **not** undefined behavior. The behavior of this program as well as this program with the try block removed from the destructor of `A` are both well defined. – Pete Becker Dec 15 '16 at 22:35
  • Are you sure this is the correct code? The `` header does not define `printf`. Once that is fixed, [here are the results](http://ideone.com/Hvo9Mf) – PaulMcKenzie Dec 15 '16 at 22:37
  • 3
    @PaulMcKenzie - `` is not **required** to define `printf`, but it is allowed to. – Pete Becker Dec 15 '16 at 22:39
  • 2
    @PaulMcKenzie - given the number of comments and answers to this question that are dead wrong, it seems perfectly appropriate to ask what the real rules are. – Pete Becker Dec 15 '16 at 22:47
  • 1
    @PaulMcKenzie: I'm not sure, experimenting is often a great way to gain insight in complex concepts; in this very case, I actually discovered several interesting facets of the issue that I wasn't aware of. Also, blindly accepting best practices without understanding what's behind them is often dangerous - as Raymond Chen says, you risk to [fall into the trap of cargo cult programming](https://blogs.msdn.microsoft.com/oldnewthing/20091104-00/?p=16153). It is true however that learning by experiment in a language with all the UB that C++ has *is* in facts a risky business. – Matteo Italia Dec 15 '16 at 22:48

2 Answers2

26

Best practice prior to C++17 says to not let exceptions propagate out of a destructor. It is fine if a destructor contains a throw expression or calls a function that might throw, as long as the exception thrown is caught and handled instead of escaping from the destructor. So your A::~A is fine.

In the case of B::~B, your program is fine in C++03 but not in C++11. The rule is that if you do let an exception propagate out of a destructor, and that destructor was for an automatic object that was being directly destroyed by stack unwinding, then std::terminate would be called. Since b is not being destroyed as part of stack unwinding, the exception thrown from B::~B will be caught. But in C++11, the B::~B destructor will be implicitly declared noexcept, and therefore, allowing an exception to propagate out of it will call std::terminate unconditionally.

To allow the exception to be caught in C++11, you would write

~B() noexcept(false) {
    // ...
}

Still, there would be the issue that maybe B::~B is being called during stack unwinding---in that case, std::terminate would be called. Since, before C++17, there is no way to tell whether this is the case or not, the recommendation is to simply never allow exceptions to propagate out of destructors. Follow that rule and you'll be fine.

In C++17, it is possible to use std::uncaught_exceptions() to detect whether an object is being destroyed during stack unwinding. But you had better know what you're doing.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • 2
    Completely confirmed by the fact that compiling in C++03 mode [gives OP's result](http://ideone.com/AlobXW), while compiling in C++14 mode [calls `terminate`](http://ideone.com/qDA4GW). g++ 6.2 in this case even gives a nice warning "warning: throw will always call terminate() [-Wterminate] note: in C++11 destructors default to noexcept". – Matteo Italia Dec 15 '16 at 22:46
18

The recommendation that "we should not throw exception in destructor" is not an absolute. The issue is that when an exception is thrown the compiler starts unwinding the stack until it finds a handler for that exception. Unwinding the stack means calling destructors for objects that are going away because their stack frame is going away. And the thing that this recommendation is about occurs if one of those destructors throws an exception that isn't handled within the destructor itself. If that happens, the program calls std::terminate(), and some folks think that the risk of that happening is so severe that they have to write coding guidelines to prevent it.

In your code this isn't a problem. The destructor for B throws an exception; as a result, the destructor for a is also called. That destructor throws an exception, but handles the exception inside the destructor. So there's no problem.

If you change your code to remove the try ... catch block in the destructor of A, then the exception thrown in the destructor isn't handled within the destructor, so you end up with a call to std::terminate().

EDIT: as Brian points out in his answer, this rule changed in C++11: destructors are implicitly noexcept, so your code should call terminate when the the B object is destroyed. Marking the destructor as noexcept(false) "fixes" this.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • Nice to know - but still, in general it's true that you don't want destructors that throw; although there are cases (like this) where you can make it work, it requires a lot of care, and restricts severely what you can do with the given object (for example, you cannot count on it being destructed safely during the unwinding due to another exception and you cannot put it safely into an STL container). – Matteo Italia Dec 15 '16 at 22:40
  • See also [this answer](https://stackoverflow.com/a/130123/3054219) or the C++ standard at 15.2 (quote is from the working draft): '(...) 3. The process of calling destructors for automatic objects constructed on the path from a try block to a throw-expression is called “stack unwinding.” If a destructor called during stack unwinding exits with an exception, std::terminate is called (15.5.1). [Note: So destructors should generally catch exceptions and not let them propagate out of the destructor. — end note"] ' – Sonic78 Oct 06 '17 at 07:31
  • "there's no problem" – unless someone else tries to use this class in totally normal ways later on. This class is a trap, plain and simple. "C allows you to shoot yourself in the foot. C++ allows you to re-use the bullet." – ech Feb 26 '19 at 19:01
  • 1
    @ech -- please don't take things out of context. "**In your code this isn't a problem.** [here's what happens] So there's no problem." The rest of my answer talks about what happens in general, and fully covers the issue that's got you so worked up. – Pete Becker Feb 26 '19 at 20:38
  • I meant it in that context. Thanks! – ech Feb 27 '19 at 03:10