If initialization or destruction is terminated by an exception which is not handled, are fully-constructed subobjects necessarily destroyed?

StackOverflow https://stackoverflow.com/questions/22137693

  •  19-10-2022
  •  | 
  •  

Question

The standard distinguishes between two forms of destruction that occur when an exception is thrown. Emphasis mine.

§15.2/1

As control passes from a throw-expression to a handler, destructors are invoked for all automatic objects constructed since the try block was entered. The automatic objects are destroyed in the reverse order of the completion of their construction.

§15.2/2

An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects (excluding the variant members of a union-like class), that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked. If the object was allocated in a new-expression, the matching deallocation function (3.7.4.2, 5.3.4, 12.5), if any, is called to free the storage occupied by the object.

§15.2/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 ]

So it seems that we have (a) stack unwinding, which destroys automatic objects, and (b) destruction of fully constructed subobjects of the object whose constructor or destructor exits via an exception, which occurs regardless of storage duration.

A careful reading of §15.2/1 suggests that stack unwinding only necessarily occurs if control passes to a handler, leaving open the possibility that stack unwinding might not occur if the exception is not handled. Indeed, §15.5.2/2 says,

In the situation where no matching handler is found, it is implementation-defined whether or not the stack is unwound before std::terminate() is called."

But the wording of §15.2/2 doesn't seem to leave open such a possibility. It simply says that the initialization or destruction has to be terminated by an exception---not that control has to pass to a handler. So my interpretation is that even if the exception is not handled, subobjects are still destroyed. Is this the correct interpretation?

For example, let's say we have

std::vector<int> V;
ComplicatedObject* p = new ComplicatedObject();

and ComplicatedObject's constructor throws, and the exception is not handled. Then whether or not V is destroyed is implementation-defined. Is it also implementation-defined whether fully constructed subobjects of *p are destroyed? Note that such objects do not have automatic storage duration.

Was it helpful?

Solution

Your interpretation is (obviously) correct, and neither Clang nor GCC conform to the standard in this scenario.
This was subject of CWG issue #1774:

The current wording of 15.5.1 [except.terminate] paragraph 2 affords implementations a significant degree of freedom when exception handling results in a call to std::terminate:

In the situation where no matching handler is found [..]

This contrasts with the treatment of subobjects and objects constructed via delegating constructos in 15.2 [except.ctor] paragraph 2:

An object of any storage duration [..]

Here the destructors must be called. It would be helpful if these requirements were harmonized.

A resolution has been proposed that didn't make it into C++14. Your quote, §15.3/11, will be deleted. Instead, §15.2 will contain

For an object of class type of any storage duration whose initialization or destruction is terminated by an exception, the destructor is invoked for each of the object’s fully constructed subobjects, that is, for each subobject for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution, except that in the case of destruction, the variant members of a union-like class are not destroyed. The subobjects are destroyed in the reverse order of the completion of their construction. Such destruction is sequenced before entering a handler of the function-try-block of the constructor or destructor, if any.

This should eliminate any doubts. Also, note that the changes are already incorporated into the current working draft, N4296.

OTHER TIPS

I'm implemented exception handling and had to read through GCC's exception handling code. I'm not exactly sure what the standard guarantees, but I know what happens here. In GCC if an exception is not handled then no stack unwinding will be done. The ABI definition for stack unwinding on Unix-like systems just ends the program if no handler is called. It doesn't unwind any stack frames.

The implementation defined aspect in the standard is to allow optimizations. It's possible that some destructors are called since they never existed in a runtime stack frame. The optimizer has reworked the code and removed some exception handling. So you could have some C++ objects that get destroyed. The amount will be really minimal though at the bottom of the stack only.

As you quoted:

The process of calling destructors for automatic objects constructed on the path from a try block to a throw-expression is called “stack unwinding.”

This includes both objects and "subobjects" (which are just automatic objects enclosed in other objects). So, the destruction of subobjects is part of the stack unwinding, because subobjects are actually "automatic objects constructed on the path from a try block to a throw-expression" themselves.

So my interpretation is that even if the exception is not handled, subobjects are still destroyed.

Subobjects will be destroyed if the implementation decides so as per §15.5.2/2 (that you also quoted).


Is it also implementation-defined whether fully constructed subobjects of *p are destroyed?

No, because they fall into the set of subobjects that the standard guarantees will be destroyed in §15.2/2.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top