Document number: N3189=10-0179 revision of N3122=10-0112
Date: 2010-11-12
Author: Daniel Krügler, additional edits by Pablo Halpern, Peter Sommerlad
Project: Programming Language C++, Library Working Group
Reply-to: Daniel Krügler

Observers for the three handler functions

Introduction

With the specification of concurrency within the C++ Standard, the library needed to define new requirements on library components and of user-code in the presence of a multi-threaded environment especially in regard to the possibility of data races. Therefore [new.delete.dataraces]/1 says:

The library versions of operator new and operator delete, user replacement versions of global operator new and operator delete, and the C standard library functions calloc, malloc, realloc, and free shall not introduce data races (1.10) as a result of concurrent calls from different threads.[..]

These requirements become a problem for a user-defined replacement of operator new that attempts to mimic some parts of the default version, especially the requirement to check the state of the new_handler as described in [new.delete.single]/1. User-code can only portably use the mutating function set_new_handler() to read the value of that handler function. This means that each call of the new operator potentially introduces a data races because user-code and library-code can rely on the contract that it is ok to allocate from the free store concurrently.

Several solutions to this problem exist, but during the Rapperswil meeting it was agreed on to provide for C++0x at least the minimum necessary, namely to provide non-mutating getter functions of the new_handler and for consistency also for the unexpected_handler and the terminate_handler. This minimum requirement could still cause data races, when either of these handlers is explicitly changed during the run-time of a program after main, but this situation is assumed to be a relatively rare situation in most programs and can still be changed in the future.

Discussion

This proposal addresses NB comments DE 14 and GB 71 and suggests to perform the following changes against the current draft:

Discussion in Batavia

Discussion in LWG concluded that wording for protecting the underlying function pointer locations should be in this paper. A sentence like "calls to get_new_handler() etc. and set_new_handler() etc shall not incur a data race" should be added to each of these handlers. Implementations can use atomic<fp> or a mutex to ensure such if multiple threads are available. Library implementations of today might start multiple threads before main() gets called. That is the reason why we want the stronger guarantee to avoid problems.

In addition Hans wants to make sure that the observable behavior is sequentially consistent. So that other things happening before a set_new_handler call setting a value of X are observable by another thread that calls set_new_handler/get_new_handler that read that value X.

thread 1:                       thread 2:
y = 42
set_new_handler(X) ----------   get_new_handler()==X
                              assert(y == 42)

also adjusted all throw() to noexcept.

Proposed resolution

  1. Change 3.7.4.1 Allocation functions [basic.stc.dynamic.allocation] as indicated:

    3 - An allocation function that fails to allocate storage can invoke the currently installed new-handler function (18.6.2.3), if any. [ Note: A program-supplied allocation function can obtain the address of the currently installed new_handler using the std::sget_new_handler function (18.6.2.4). — end note ]

  2. Change 17.6.3.7 Handler functions [handler.functions] as follows:

    1 The C++ standard library provides default versions of the following handler functions (Clause 18):

    • unexpected_handler
    • terminate_handler

    2 A C++ program may install different handler functions during execution, by supplying a pointer to a function defined in the program or the library as an argument to (respectively):

    • set_new_handler
    • set_unexpected
    • set_terminate

    3 A C++ program may query the current handler function pointer with the following functions (respectively):

    • get_new_handler
    • get_unexpected
    • get_terminate

    Calling the "set_" functions or the corresponding "get_" functions shall not incur a data race. Each "set_" function synchronizes with subsequent calls to the corresponding "set_" or "get_" function taking the value set.

    See also: subclauses 18.6.2, Storage allocation errors, and 18.8, Exception handling.

  3. Change 18.6 [support.dynamic], header <new> synopsis as indicated:

      namespace std {
        class bad_alloc;
        class bad_array_new_length;
        struct nothrow_t {};
        extern const nothrow_t nothrow;
        typedef void (*new_handler)();
        new_handler get_new_handler() noexcept;
        new_handler set_new_handler(new_handler new_p) throw()noexcept;
      }
      ...
    

  4. Relax the current specification of the default behaviour of operator new such that implementations have more freedom to realize the same effects:

    18.6.1.1 Single-object forms [new.delete.single]

    void* operator new(std::size_t size) throw(std::bad_alloc);
    

    [...]

    4 Default behavior:

    • Executes a loop: Within the loop, the function first attempts to allocate the requested storage. Whether the attempt involves a call to the Standard C library function malloc is unspecified.
    • Returns a pointer to the allocated storage if the attempt is successful. Otherwise, if the argument in the most recent call to set_new_handler() ([set.new.handler]) wascurrent new_handler ([get.new.handler]) is a null pointer value, throws bad_alloc.
    • Otherwise, the function calls the current new_handler function (18.6.2.3). If the called function returns, the loop repeats.
    • The loop terminates when an attempt to allocate the requested storage is successful or when a called new_handler function does not return.
  5. Change 18.6.2.4/2 as indicated for better clarification that the initial new_handler is a null pointer. Following 18.6.2.4, [set.new.handler], insert a new subclause as indicated:

    18.6.2.4 set_new_handler [set.new.handler]

    new_handler set_new_handler(new_handler new_p) throw()noexcept;
    

    1 Effects: Establishes the function designated by new_p as the current new_handler.

    2 Returns: 0 on the first call, the previous new_handler on subsequent callsThe previous new_handler.

    3 Remarks: The initial new_handler is a null pointer.

    18.6.2.5 get_new_handler [get.new.handler]

    new_handler get_new_handler() noexcept;
    

    1 Returns: The current new_handler. [Note: This may be a null pointer value — end note].

  6. Change 18.8 [support.exception], header <exception> synopsis as indicated:

      namespace std {
        ...
        typedef void (*unexpected_handler)();
        unexpected_handler get_unexpected() noexcept;
        unexpected_handler set_unexpected(unexpected_handler f) throw()noexcept;
        void unexpected [[noreturn]] ();
        typedef void (*terminate_handler)();
        terminate_handler get_terminate() noexcept;
        terminate_handler set_terminate(terminate_handler f) throw()noexcept;
        void terminate [[noreturn]] ();
        ...
      }
      ...
    

  7. Keep 18.8.2.2 [unexpected.handler] unchanged:

    typedef void (*unexpected_handler)();
    

    1 The type of a handler function to be called by unexpected() when a function attempts to throw an exception not listed in its dynamic-exception-specification.

    2 Required behavior: An unexpected_handler shall not return. See also 15.5.2.

    3 Default behavior: The implementation's default unexpected_handler calls terminate().

  8. Replace the requirement that the new handler shall be not null by a normative remark that doing so has unspecified effects. Following 18.8.2.3, [set.unexpected], insert a new subclause as indicated:

    18.8.2.3 set_unexpected [set.unexpected]

    unexpected_handler set_unexpected(unexpected_handler f) throw()noexcept;
    

    1 Effects: Establishes the function designated by f as the current unexpected_handler.

    2 Requires: f shall not be a null pointer.Remarks: It is unspecified whether a null pointer value designates the default unexpected_handler.

    3 Returns: The previous unexpected_handler.

    18.8.2.4 get_unexpected [get.unexpected]

    unexpected_handler get_unexpected() noexcept;
    

    1 Returns: The current unexpected_handler. [Note: This may be a null pointer value — end note].

  9. Change 18.8.2.4 [unexpected] (that has now been renumbered to 18.8.2.5) as indicated: The suggested rewording is supposed to achieve the following effects: Paragraph 1 does now solely concentrate on the conditions, when unexpected is called, and paragraph 2 now solely concentrates on the call effects and clarifies that even a null default handler behaves as-if it were a non-null pointer.

    18.8.2.4 unexpected [unexpected]

    void unexpected [[noreturn]] ();
    

    1 Remarks: Called by the implementation when a function exits via an exception not allowed by its exception-specification (15.5.2), in effect immediately after evaluating the throw-expression (18.8.2.2). May also be called directly by the program.

    2 Effects: Calls the unexpected_handler function in effect immediately after evaluating the throw-expression (18.8.2.2), if called by the implementation, or calls the current unexpected_handler, if called by the program.Calls the current unexpected_handler function. [Note: A default unexpected_handler is always considered a callable handler in this context — end note]

  10. Keep 18.8.3.1 [terminate.handler] unchanged:

    typedef void (*terminate_handler)();
    

    1 The type of a handler function to be called by terminate() when terminating exception processing.

    2 Required behavior: A terminate_handler shall terminate execution of the program without returning to the caller.

    3 Default behavior: The implementation's default terminate_handler calls abort().

  11. Following 18.8.3.2, [set.terminate], insert a new subclause as indicated:

    18.8.3.2 set_terminate [set.terminate]

    terminate_handler set_terminate(terminate_handler f) throw()noexcept;
    

    1 Effects: Establishes the function designated by f as the current handler function for terminating exception processing.

    2 Requires: f shall not be a null pointer.Remarks: It is unspecified whether a null pointer value designates the default terminate_handler.

    3 Returns: The previous terminate_handler.

    18.8.3.3 get_terminate [get.terminate]

    terminate_handler get_terminate() noexcept;
    

    1 Returns: The current terminate_handler. [Note: This may be a null pointer value — end note].

  12. Change 18.8.3.3 [terminate] (that has now been renumbered to 18.8.3.4) as indicated. The suggested rewording is supposed to achieve the following effects: Paragraph 1 does now solely concentrate on the conditions, when terminate is called, and paragraph 2 now solely concentrates on the call effects and clarifies that even a null default handler behaves as-if it were a non-null pointer.

    18.8.3.3 terminate [terminate]

    void terminate [[noreturn]] ();
    

    1 Remarks: Called by the implementation when exception handling must be abandoned for any of several reasons (15.5.1), in effect immediately after evaluating the throw-expression (18.8.3.1). May also be called directly by the program.

    2 Effects: Calls the terminate_handler function in effect immediately after evaluating the throw-expression (18.8.3.1), if called by the implementation, or calls the current terminate_handler function, if called by the program.Calls the current terminate_handler function. [Note: A default terminate_handler is always considered a callable handler in this context — end note]