Skip to content

Latest commit

 

History

History
84 lines (57 loc) · 5.45 KB

P0770R0.md

File metadata and controls

84 lines (57 loc) · 5.45 KB

Document number: P0770R0

Date: 2017-08-21

Audience: CWG, EWG

Reply-to: Michael Kilburn <crusader.mike at gmail dot com>

A Proposal to Specify Behavior in Case of Exception Allocation Failure

1. Introduction (and motivation)

Memory for exception object is allocated in an 'unspecified way' (18.1/4). This makes it impossible for application to handle such allocation failures. If implementation uses dynamic storage for exception this makes it impossible to write code that reliably handles lack of space in dynamic storage (aka out-of-memory (OOM)).

On the other hand code that uses C-style error handling doesn't have this issue. This proposal aims to "close the gap" by mandating certain behavior in case of exception allocation failure and providing tools to handle it.

2. Impact On the Standard

Failure to allocate memory for exception being thrown will have a well-defined behavior.

3. Problem

Consider this snippet in context of typical stack-based implementation (e.g. MSVC/GCC):

int foo() { return -1; }
...
int res = foo();
if (res)
    return res;

and this one:

void foo() { throw -1; }
...
foo();

First snippet can have only two outcomes:

  • stack overflow if there is no space on stack for another frame
  • error object (-1) is successfully returned to caller

Second snippet can have three outcomes:

  • stack overflow
  • exception object (-1) is successfully constructed and thrown
  • unspecified if memory allocation for exception object fails, no way to handle something unspecified in the code

With GCC (which allocates exceptions in dynamic storage and calls std::terminate() on failure) this means you can't write code that reliably survives OOM. It is understandable why GCC chose std::terminate() (over throwing std::bad_alloc, for example) -- no one expects throw -1 to throw something else instead.

Note that fundamentally these two snippets do the same thing -- construct error object and pass it to caller. The important difference that in first one object memory is already preallocated by caller and in second one -- it gets allocated by callee when it is time to throw.

4. Solution

In short:

  • introduce new exception XX (eXceptional eXception) that is guaranteed to be thrown without fail
  • change 18.1/4 to mandate that throw ABC will throw XX if runtime can't allocate memory for ABC
  • (optional) clarify 21.8.6/8 -- mention that XX can be thrown if memory for std::bad_exception can't be allocated

Subsequent sections describe "problem points" discovered so far.

4.1 New exception

It is better to (re)use std::bad_alloc for XX. But it may be useful to distinguish between running out of dynamic storage and running out of "exception storage" -- if it is important, std::bad_alloc can be extended with additional member variable that specifies storage we ran out of.

Using std::bad_alloc means stdlib doesn't need to be changed to accomodate new exception.

(Regardless of choice made wrt std::bad_alloc) XX support should be built in such way that throwing it does not consume storage used for normal exception mechanisms. If it is impossible to implement -- this proposal can be disregarded.

With GCC (which uses Itanium C++ ABI), no-fault throw XX should be possible -- for example by passing NULL into __cxa_throw() and coding internals accordingly.

4.2 Ability to detect 'out of exception storage' condition

Implementation must be able to detect allocation failure. It is expected that it shouldn't be a problem (certainly not for GCC), but if it is (for example if exception gets allocated on stack with unknown size) -- this proposal may need an update.

4.3 std::current_exception()

std::current_exception() is declared noexcept and returns (equivalent of) NULL pointer if no exception is being handled right now. In some implementations it makes a copy of exception object, which can throw and fail to allocate memory for exception -- in this case returned exception_ptr should represent XX (probably a pointer to statically allocated object or magical value recognized by rethrow_exception()/etc)

4.4 Existing code

Only very small portion of existing code can be expected to "break" -- in a sense that unspecified behaviour of an extremely rare situation will get replaced with defined (but somewhat surprising) behavior. Majority of code is written to handle exceptions in general and will not need to be changed. Also, typically user exceptions have throwing copy constructors and it is expected that throw MyException(...) can throw std::bad_alloc.

4.5 General thoughts on implementations

Implementations using dynamic storage for exceptions (like GCC) should not have any problems.

Implementations that use call stack as exception storage (like MSVC) could be considered problematic since C++ traditionally doesn't provide means of handling out-of-stack condition (probably because this will make impossible to create no-fail functions). My belief that it shouldn't be a game changer since at time of allocation implementation should know how much stack has been left and should be able to check it.

Implementation can also use a separate stack for exceptions -- to be created on first throw (in current thread) with every allocation checked against remaining free space. This will allow stack unwinding to work very similar to C-style error propagation -- by dropping stack frames one-by-one as unwinding makes progress (thus making it available to be used for calling destructors).

5. Proposed Text

TBD