Reputation: 17223
I have read that it is not a good idea to throw from a destructor because of stack unwinding. I am not sure I understand that fully. So I tried the following example
struct foo
{
~foo()
{
throw 1;
}
};
struct bar
{
~bar()
{
throw 2;
}
};
int main()
{
try
{
foo a;
bar b;
throw 3;
}catch(int a)
{
std::cout << a;
}
}
Now I was expecting that the a would be 1 because first 3 is thrown then destructor of b is called which throws 2 and then destructor of a is called which throws 1. Apparently this is not the case and this might explain why its not a good idea to throw from destructors. My question is why was abort() called the destructor of b was called ?
Upvotes: 3
Views: 526
Reputation: 158469
Throwing an exception during stack-unwinding this will lead to std::terminate
being called whose default action is to call std::abort
.
CERT has a good explanation in their ERR33-CPP. Destructors must not throw exceptions document which says (emphasis mine):
A destructor is very likely to be called during stack unwinding resulting from an exception being thrown. If the destructor itself throws an exception, having been called as the result of an exception being thrown, then the function std::terminate() is called with the default effect of calling std::abort(). This could provide the opportunity for a denial-of-service attack. Hence, destructors must satisfy the no-throw guarantee, that is, they must not throw an exception if they themselves have been called as the result of an exception being thrown.
This is covered in the draft C++ standard section 15.2
Constructors and destructors which says:
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 ]
Note that in C++11 destructors are specified implicitly noexcept(true)
as long as none of the functions it calls allows exceptions. So in this case throwing from a destructor would call std::terminate
regardless.
From section 12.4
Destructors:
A declaration of a destructor that does not have an exception-specification is implicitly considered to have the same exception-specification as an implicit declaration (15.4).
and 15.4
says:
An implicitly declared special member function (Clause 12) shall have an exception-specification. If f is an implicitly declared default constructor, copy constructor, move constructor, destructor, copy assignment operator, or move assignment operator, its implicit exception-specification specifies the type-id T if and only if T is allowed by the exception-specification of a function directly invoked by f’s implicit definition; f shall allow all exceptions if any function it directly invokes allows all exceptions, and f shall allow no exceptions if every function it directly invokes allows no exceptions.
Theoretically you could use std::uncaught_exception to detect stack-unwinding in the destructor but in GotW #47 Herb Sutter explains why this technique is not as useful as it seems.
Although Herb has very recently proposed a fix in N4152: uncaught _exceptions
Upvotes: 6
Reputation: 4098
On some cases it is possible to add an exception specification like this and handle them with safety. Note that the object is only deleted when no exception is thrown.
#include<iostream>
using namespace std;
struct A{
bool a;
A():a(1){}
~A()throw(int){
if(a)a=0,throw 0;
}
};
int main(){
A*a=new A();
try{
delete a;
}catch(int&){
delete a;
cout<<"here"<<endl;
}
return 0;
}
Upvotes: 0
Reputation: 7951
Others have answered from the standard, but I think another example illustrates the conceptual problem best:
struct foo {
char *buf;
foo() : buf(new char[100]) {}
~foo() {
if(buf[0] == 'a')
throw 1;
delete[] buf;
}
};
int main() {
foo *f = new f;
try {
delete f;
} catch(int a) {
// Now what?
}
}
This is a bit simplistic, of course, but shows the problem. What's the right thing for delete f
to have done? Whichever way it goes, you end up with either a partially destructed object or a memory leak.
Upvotes: 0
Reputation: 308158
Whenever you throw an exception while exception processing is ongoing, you get a special exception that can't be caught and this leads to an abort.
You can use std::uncaught_exception
to detect if exception handling is already in progress and avoid throwing in that case.
Upvotes: 3