38

From C++11 standard (15.1.p4):

The memory for the exception object is allocated in an unspecified way, except as noted in 3.7.4.1

What if allocation fails -- will it throw std::bad_alloc instead? Call std::terminate? Unspecified?

jotik
  • 17,044
  • 13
  • 58
  • 123
C.M.
  • 3,071
  • 1
  • 14
  • 33
  • 1
    FWIW, the [size of the exception object can be quite big](https://wandbox.org/permlink/qYAantWcpSmTAtyd), and if you try to allocate something too big, it [appears that `std::terminate` is called](https://wandbox.org/permlink/OqNb9bru9VcI9Ypx). Whether this is the specified behavior in the standard I'm unsure. – Justin Aug 04 '17 at 03:56
  • In a less extreme example, not involving a stack overflow, I would expect a `bad_alloc`. Also, some compilers preallocate space for a "reasonable" amount of exceptions. – Bo Persson Aug 04 '17 at 08:46
  • @BoPersson Expectations are irrelevant -- does standard specify what is going to happen? Look like an omission that needs to be fixed – C.M. Aug 04 '17 at 16:22
  • The Itanium ABI specifies that `__cxa_allocate_exception` calls `terminate` if it cannot allocate memory. – T.C. Aug 04 '17 at 16:58
  • 1
    @T.C. If this behavior is OK from standard POV then it is impossible to write reliable C++ code. I can't find anything that forbids or allows std::teminate() in this case. – C.M. Aug 05 '17 at 01:19

4 Answers4

15

(providing my own answer... I'll wait for few days and if there are no problems with it -- I'll mark it as accepted)

I spent some time investigating this and here is what I unearthed:

  • C++ standard does not specify what is going to happen in this case
  • Clang and GCC seem to use C++ Itanium ABI

Itanimum ABI suggests to use heap for exceptions:

Storage is needed for exceptions being thrown. This storage must persist while stack is being unwound, since it will be used by the handler, and must be thread-safe. Exception object storage will therefore normally be allocated in the heap

...

Memory will be allocated by the __cxa_allocate_exception runtime library routine.

So, yeah... throwing an exception will likely involve locking mutexes and searching for a free memory block. :-(

It also mentions this:

If __cxa_allocate_exception cannot allocate an exception object under these constraints, it calls terminate()

Yep... in GCC and Clang "throw myX();" can kill your app and you can't do a thing about it (maybe writing your own __cxa_allocate_exception can help -- but it certainly won't be portable)

It gets even better:

3.4.1 Allocating the Exception Object

Memory for an exception object will be allocated by the __cxa_allocate_exception runtime library routine, with general requirements as described in Section 2.4.2. If normal allocation fails, then it will attempt to allocate one of the emergency buffers, described in Section 3.3.1, under the following constraints:

  • The exception object size, including headers, is under 1KB.
  • The current thread does not already hold four buffers.
  • There are fewer than 16 other threads holding buffers, or this thread will wait until one of the others releases its buffers before acquiring one.

Yep, your program can simply hang! Granted chance of this are small -- you'd need to exhaust memory and your threads need to use up all 16 emergency buffers and enter wait for another thread that should generate an exception. But if you do things with std::current_exception (like chaining exception and passing them between threads) -- it is not so improbable.

Conclusion:

This is a deficiency in C++ standard -- you can't write 100% reliable programs (that use exceptions). Text book example is a server that accepts connections from clients and executes submitted tasks. Obvious approach to handle problems would be to throw an exception, which will unwind everything and close connection -- all other clients won't be affected and server will continue to operate (even under low memory conditions). Alas, such server is impossible to write in C++.

You can claim that modern systems (i.e. Linux) will kill such server before we reach this situation anyway. But (1) it is not an argument; (2) memory manager can be set to overcommit; (3) OOM killer won't be triggered for 32-bit app running on 64bit hardware with enough memory (or if app artificially limited memory allocation).

On personal note I am quite pissed about this discovery -- for many years I claimed that my code handles out-of-memory gracefully. Turns out I lied to my clients. :-( Might as well start intercepting memory allocation, call std::terminate and treat all related functions as noexcept -- this will certainly make my life easier (coding-wise). No wonder they still use Ada to programs rockets.

C.M.
  • 3,071
  • 1
  • 14
  • 33
  • 1
    Unfortunately, use of Ada didn't help the [Ariane 5 rocket](https://itsfoss.com/a-floating-point-error-that-caused-a-damage-worth-half-a-billion/) when an uncaught exception turned the system off. – Bo Persson Aug 15 '17 at 14:49
  • 2
    @BoPersson This is because they had a bug in the code. Point of that rant was that with C++ even a 100% **correct** code can still crash. – C.M. Aug 15 '17 at 16:20
  • 1
    About the last few paragraphs... You are claiming the implementation detail of Itanium ABI is a deficiency in the standard. I would say it is a deficiency in the implementation. – André Oct 15 '17 at 09:05
  • 2
    @André afaik standard allows any behaviour in this situation -- therefore Itanium ABI is fine. But since *any behavior* means *unusable for practical purposes* -- I consider it a deficiency in C++ standard. – C.M. Oct 15 '17 at 18:22
  • I think C++ runtime might be allowed to allocate exception object on stack and copy it during stack unwinding once per each catch block. Standard does not allow exception copy constructor to throw anyway, so such implementation should be reliable. –  Oct 16 '17 at 02:02
  • Are you going to submit a defect report? – Sebastian Redl Oct 17 '17 at 10:12
  • Imo if this is treated as deficiency, then the only possible solution to it is to force all implementations to move exception allocation to the stack. At this point GCC developers can argue that memory reservation is no different from stack reservation, just with different limitations. So I would say writing reliable program is possible if you use proper compiler, just as it is possible to write program that never causes stack overflow. –  Oct 17 '17 at 10:25
  • 1
    @SebastianRedl I've submitted a proposal (P0770R0) to address this few weeks ago. Was too busy with other stuff, not sure what has happened to it. – C.M. Oct 21 '17 at 03:37
  • @Ivan Well, I am not sure if MSVC approach is a good one -- maybe it makes it impossible to estimate required stack size? Also, there are other possibilities (check [this proposal](https://github.com/crusader-mike/misc/blob/master/P0770R0.md), for example) – C.M. Oct 21 '17 at 03:38
  • @C.M. C++ code that switches execution context (like Boost.Context) usually uses custom exception to unwind stack on coroutine destruction Now if "throw unwind_exception" throws something else such coroutine won't be able to delete itself since it might not know to which context execution should be passed afterwards. Calling std::terminate is the only option here. Imo "throw X" should always work just like "return X" works. –  Oct 21 '17 at 14:03
  • @Ivan ...or they'll have to change that coroutine design. – C.M. Oct 21 '17 at 17:04
  • @C.M. This design is way too convenient, easier to fix C++ exceptions. Throwing bad alloc also has a huge disadvantage - it discards error information and makes it impossible to use exceptions in situations when this is not acceptable. This alone defeats the whole purpose of this proposal - you can't write reliable program like this. –  Oct 21 '17 at 17:22
  • @C.M. Besides, bad alloc cannot be "magically" thrown, even GCC relies on preallocated buffers to do this and terminates program when there is none left. It doesn't solve the problem - there should be a way to guarantee exception throw, if this can be done for bad alloc, no reason not to apply the same technique for user defined exceptions. –  Oct 21 '17 at 17:25
  • @Ivan `bad alloc cannot be "magically" thrown` -- I believe it can be. `you can't write reliable program like this` -- yes you can. You just take this possibility into account. – C.M. Oct 21 '17 at 20:07
  • @C.M. And what do you do with in coroutine example a gave before? Sometimes you just have to handle errors. Now you can say that exceptions shouldn't be used in those cases, but I feel this is an another topic. You are either guaranteed to report error when it matters or the whole mechanism is broken, so you can just leave it as broken as it is. As for magically throwing bad alloc - C++ also allows you to not use standard exceptions, introducing your own hierarchy. This makes bad alloc a very special exception that cannot be avoided. –  Oct 21 '17 at 21:21
  • @Ivan `This makes bad alloc a very special exception that cannot be avoided` -- precisely. And if you prefer terminate instead bad_alloc -- we can provide override similar to `set_new_exception_handler`. I seriously don't care if that coroutine design is not going to work 100% -- it is already **not working** 100%. As in: "it can crash if you run out of memory and you can't do anything about it". – C.M. Oct 22 '17 at 04:07
  • @C.M. Not really, it is 100% working with MSVC implementation, you just need to show that there is enough stack - you have to do this anyway. And it is not about coroutines, it is about writing programs that actually handle errors and proceed instead of just logging and exiting. Any exception mechanism is already hard to use in such programs, now it won't be even work. –  Oct 22 '17 at 10:29
  • It seems that P0709 also addresses this issue. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r2.pdf – Sherwood Wang Feb 01 '19 at 05:45
  • 1
    @SherwoodWang Not really... It mentions the problem of reporting heap exhaustion with heap-allocated exception in 4.3, but (1) it has very little details on why it is bad and (2) I don't see a proposed solution for "classic" code (i.e. code that expects `std::bad_alloc` on allocation failure). As of now, I believe that granting special status to `std::bad_alloc` (that is: it should not consume any memory -- as in "it should be represented by `NULL` in underlying machinery") is a better solution. – C.M. Feb 21 '19 at 17:22
3

[intro.compliance]/2 Although this International Standard states only requirements on C++ implementations, those requirements are often easier to understand if they are phrased as requirements on programs, parts of programs, or execution of programs. Such requirements have the following meaning:

(2.1) — If a program contains no violations of the rules in this International Standard, a conforming implementation shall, within its resource limits, accept and correctly execute that program.

Emphasis mine. Basically, the standard envisions failure to allocate dynamic memory (and prescribes behavior in this case), but not any other kind of memory; and doesn't prescribe in any way what the implementation should do when its resource limits are reached.

Another example is running out of stack due to a too-deep recursion. Nowhere does the standard say how deep a recursion is allowed. The resulting stack overflow is the implementation exercising its "within resource limits" right-to-fail.

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • 1
    I could easily see and control how deep my stack will be. I have no good way to estimate how much of "invisible exception heap" have been used in my program -- libraries can accumulate exception objects (via `std::exception_ptr`). Each thread can accumulate a few. No good way to estimate usage of this "invisible heap" just by looking at my code. Imho, it is not OK to terminate in this case -- standard needs to be updated to provide better guarantees. – C.M. Aug 05 '17 at 18:47
  • 2
    Comparing OOM to stack overflow seems to be a common mistake. The difference is that stack usage does not depend on the size of input data (unless recursive algorithms are used), but heap usage usually does depend on it. –  Oct 16 '17 at 02:08
  • 1
    @C.M.: If you don't know what your libraries do (in terms of accumulating exception objects) I don't see how you can know how much stack space they will need either. – MikeMB Mar 06 '18 at 19:09
  • @MikeMB I didn't imply that I have no knowledge of libraries code. Point was that there is "no good way to estimate how much of 'invisible exception heap' have been used in my program". E.g. I can "pin" exceptions with "std::current_exception" and accumulate them somewhere. On the other hand stack usage can be figured out by looking at the code (well... unless you use implementation-specific stuff like `alloca()`/etc, or unless your recursion depth depends on input data). – C.M. Mar 07 '18 at 02:26
  • 1
    @C.M. By the same token, you can estimate how much of "invisible exception heap" is used by your program (well ... unless you use `std::current_exception` to pin exceptions and accumulate them somewhere). Avoid doing that, just like you avoid recursion whose depth depends on input data. I'm not sure why you consider one bad practice as OK to hand-wave away, while another as sky-is-falling horrible. The bad use of `std::current_exception` is much less common than the bad use of recursion, in my experience; in fact, I don't think I've ever seen anyone use `std::current_exception` in real code. – Igor Tandetnik Mar 07 '18 at 03:26
  • @C.M. A recursion depth that depends on input data is actually quite common (although it is usually logarithmic). I really don't see, why stack usage is easier to determine by looking at the code than number of exceptions in flight at the same time. – MikeMB Mar 07 '18 at 13:28
  • @MikeMB because it is harder (if not impossible) to determine how many exceptions can be in flight at the same time by looking at the code. Plus, because C++ provides no means for me to increase "invisible exception heap" -- I can't do it in a platform-independent way. And it seems on some platforms (GCC/etc) you can't even do it in platform-dependent way either. Which, ultimately leads to my point -- you can't write a code that is guaranteed to handle OOMs. – C.M. Mar 08 '18 at 02:27
  • @IgorTandetnik `std::current_exception` is just an obvious "poster child". I am not "hand-waving away" a thing. My point is that right now I simply cannot write a C++ program that is guaranteed to handle OOMs on all platforms (unless I avoid using exceptions completely). I think I can do it for MSVC (not sure tbh -- estimating max stack size while taking into account multiple simultaneous stack unwindings is tricky). I maybe can do it for GCC -- if somehow I make sure that my program is guaranteed to never break logic in p3.4.1 of Itanium ABI. But I simply can't do it in platform indep..t way. – C.M. Mar 08 '18 at 02:35
2

Current answer already describes what GCC does. I have checked MSVC behavior - it allocates exception on the stack, so allocation does not depend on the heap. This makes stack overflow is possible (exception object can be big), but stack overflow handling is not covered by standard C++.

I used this short program to examine what happens during exception throw:

#include <iostream>

class A {
public:
    A() { std::cout << "A::A() at " << static_cast<void *>(this) << std::endl; }
    A(const A &) { std::cout << "A::A(const A &) at " << static_cast<void *>(this) << std::endl; }
    A(A &&) { std::cout << "A::A(A &&) at " << static_cast<void *>(this) << std::endl; }
    ~A() { std::cout << "A::~A() at " << static_cast<void *>(this) << std::endl; }
    A &operator=(const A &) = delete;
    A &operator=(A &&) = delete;
};

int main()
{
    try {
        try {
            try {
                A a;
                throw a;
            } catch (const A &ex) {
                throw;
            }
        } catch (const A &ex) {
            throw;
        }
    } catch (const A &ex) {
    }
}

When build with GCC output clearly shows that exception thrown is being allocated far from stack:

A::A() at 0x22cad7
A::A(A &&) at 0x600020510
A::~A() at 0x22cad7
A::~A() at 0x600020510

When build with MSVC output shows that exception is allocated nearby on the stack:

A::A() at 000000000018F4E4
A::A(A &&) at 000000000018F624
A::~A() at 000000000018F4E4
A::~A() at 000000000018F624

Additional examination with debugger shows that catch handlers and destructors are executed on the top of the stack, so stack consumption grows with each catch block starting with the first throw and until std::uncaught_exceptions() becomes 0.

Such behavior means that correct out of memory handling requires you to prove there is enough stack space for the program to execute exception handlers and all destructors on the way.

To prove the same with GCC it seems that you will need to prove there no more than four nested exceptions and exceptions have size less than 1KiB (this includes header). Additionally if some thread has more than four nested exceptions, you also need to prove there is no deadlock caused by emergency buffer allocation.

  • So MSVC followes what is said in the c++ standard! – Oliv Oct 16 '17 at 06:31
  • According to your answer it looks like GCC is just non-conforming in this regard. As much as I hate MSVC and its runtime, it wins here over GCC. –  Oct 16 '17 at 06:34
  • I also use GCC on Linux, and I have abandoned to use MSVC because there are too much nonconformties: too much work to make a code compile on MSVC, too much work to maintain it and too many bugs in the executable that just come from MSVC bugs! This is the first case were I see MSVC do a better job than GCC. I hope MSVC will go into the party soon, I know they are working on it. GCC seems to benefit of competition. – Oliv Oct 16 '17 at 06:43
  • @Ivan -- question was about C++ standard, not particular implementation. I know MSVC uses stack for exceptions and this makes it immune to this problem, but because of that now it is very hard (or impossible) to estimate how much stack space my program needs -- which means if my guess is wrong, result is the same as with GCC (crash). Btw, both GCC and MSVC conform to standard here – C.M. Oct 17 '17 at 02:12
  • @Ivan Your answer provides good MSVC-specific info. If you update it a bit -- I'll mark it too. In particular these points need some improvement: (1) it is not really expected -- turns out standard *allows* it to be "dependent on compiler"; (2) allocation can fail -- just failure is different (running out of stack); (3) not only catch-handlers, but dtors also executed on top of the stack; (4) MSVC does conform standard here – C.M. Oct 17 '17 at 02:49
  • I have updated my answer. "Expected" was said about it being compiler dependent, I have removed that sentence. As for stack overflow - since C++ does not specify any handling for this, this is program responsibility to avoid stack overflow. Imo proving this is simpler and more natural than dealing with what GCC does. But I wonder if someone tried to use C++ with exceptions for kernel development where your stack space is very limited and you cannot just crash on memory failed allocation. –  Oct 17 '17 at 10:13
  • The problem with GCC I see here is that allocation logic changes its behavior when you are out of memory. Besides, if you need your program to be this robust, you have to prove there is enough stack space anyway. –  Oct 17 '17 at 10:22
  • @Ivan The real problem is that standard is written in such way that it is impossible to write a (standard-compliant and portable) program that is guaranteed to gracefully handle this situation (running out of memory used for exceptions). Thanks for looking into MSVC details, though :) – C.M. Oct 17 '17 at 17:13
  • Maybe. But using only standard as a reference it is also impossible to write program that does not cause stack overflow. After all stack size is compiler and platform dependent. There are always assumptions about your platform's capabilities. So maybe it is not such a big deal. Although GCC behaviour surprised me. –  Oct 17 '17 at 19:11
  • @Ivan Apologies, I thought I could mark two answers as correct. :-\ – C.M. Oct 18 '17 at 00:56
  • @CM _I know MSVC uses stack for exceptions ... now it is very hard (or impossible) to estimate how much stack space my program needs_ Hardly. How many nested exceptions do you expect to throw? – Paul Sanders Jun 28 '18 at 07:49
  • @PaulSanders I don't think there is a specific limit on that in a standard ;) So you should be able to have as many of them as you can imagine (until you run out of related resources -- typically stack space). – C.M. Feb 21 '19 at 17:25
-1

Actualy it is specified that if allocation for the exception object fails, bad_alloc should be thrown and implementation could also call the new handler.

This is what is actualy specified in the c++ standard section (§3.7.4.1) you site [basic.stc.dynamic.allocation]:

An allocation function that fails to allocate storage can invoke the currently installed new-handler function (21.6.3.3), if any. [ Note: A program-supplied allocation function can obtain the address of the currently installed new_handler using the std::get_new_handler function (21.6.3.4). — end note ] If an allocation function that has a non-throwing exception specification (18.4) fails to allocate storage, it shall return a null pointer. Any other allocation function that fails to allocate storage shall indicate failure only by throwing an exception (18.1) of a type that would match a handler (18.3) of type std::bad_alloc (21.6.3.1).

Then this recalled in [except.terminate]

In some situations exception handling must be abandoned for less subtle error handling techniques. [ Note: These situations are: — (1.1) when the exception handling mechanism, after completing the initialization of the exception object but before activation of a handler for the exception (18.1)*

So the itanium ABI does not follow the c++ standard specification, since it can block or call terminate if the program fails to allocate memory for the exception object.

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • first paragraph of **6**.7.4.1 states that "An allocation function shall be a class member function or a global function;" -- don't see how it applies to *throw-expression*, there is nothing in text that requires throw-expression to use allocation function from 6.7.4.1 – C.M. Oct 15 '17 at 18:33
  • 18.5.1.p(1-1) has nothing to do with my question either – C.M. Oct 15 '17 at 18:34
  • @C.M. The y changed the numbering of sections in the new standard. 3.7.4.1 -> 6.7.4.1. I have put a link to the section you cite in your question... Yes it does apply to your question (you cite it yourself!). So the good practice when you cite the standard is not to use the section number, but the section name 3.7.4.1 is [basic.stc.dynamic.allocation]. – Oliv Oct 16 '17 at 06:12
  • @C.M. Compiler must followes the fundamental rules describe in [intro.execution], it must execute what is asked and continue execution (there are progress guarentees). For exemple in [expr.add] it is not stated that "1+1" can cause the program termination or the program to block. The fact it is not stated does not mean that a compiler could is allowed to block or allowed to terminate because of it executes `1+1`. The same applies to a throw expression!! – Oliv Oct 16 '17 at 06:28
  • @C.M. It seems your are not the first to ask this question so they have recalled in [except.terminate] what can cause a program termination, and object exception allocation is explicitly excluded from the set of operation that can cause terminate. – Oliv Oct 16 '17 at 06:28
  • I clicked on link in your post -- it clearly shows 6.7.4.1. Do I need to post a screenshot? – C.M. Oct 17 '17 at 02:06
  • "The same applies to a throw expression" -- no, it doesn't – C.M. Oct 17 '17 at 02:07
  • @C.M. Have you read the comment above, where it is written "3.7.4.1 -> 6.7.4.1"? You should delete your comments, you are about to have a terrible reputation. But leave your answer, it is realy instructing, I did not know about this trouble in the itanium ABI specification. But please edit your answer to say that this is a problem specific to this ABI and not the c++ standard. – Oliv Oct 17 '17 at 06:34