What happens if 'throw' fails to allocate memory for exception object?

后端 未结 4 416
走了就别回头了
走了就别回头了 2020-11-30 01:48

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

相关标签:
4条回答
  • 2020-11-30 02:15

    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.

    0 讨论(0)
  • 2020-11-30 02:17

    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.

    0 讨论(0)
  • 2020-11-30 02:19

    (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.

    0 讨论(0)
  • 2020-11-30 02:21

    [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.

    0 讨论(0)
提交回复
热议问题