2370. Operations involving type-erased allocators should not be noexcept in std::function

Section: 22.10.17.3 [func.wrap.func] Status: Resolved Submitter: Pablo Halpern Opened: 2014-02-27 Last modified: 2020-09-06

Priority: 3

View all other issues in [func.wrap.func].

View all issues with Resolved status.

Discussion:

The following constructors in 22.10.17.3 [func.wrap.func] are declared noexcept, even though it is not possible for an implementation to guarantee that they will not throw:

template <class A> function(allocator_arg_t, const A&) noexcept;
template <class A> function(allocator_arg_t, const A&, nullptr_t) noexcept;

In addition, the following functions are guaranteed not to throw if the target is a function pointer or a reference_wrapper:

template <class A> function(allocator_arg_t, const A& a, const function& f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

In all of the above cases, the function object might need to allocate memory (an operation that can throw) in order to hold a copy of the type-erased allocator itself. The first two constructors produce an empty function object, but the allocator is still needed in case the object is later assigned to. In this case, we note that the propagation of allocators on assignment is underspecified for std::function. There are three possibilities:

  1. The allocator is never copied on copy-assignment, moved on move-assignment, or swapped on swap.

  2. The allocator is always copied on copy-assignment, moved on move-assignment, and swapped on swap.

  3. Whether or not the allocator is copied, moved, or swapped is determined at run-time based on the propagate_on_container_copy_assignment and propagate_on_container_move_assignment traits of the allocators at construction of the source function, the target function, or both.

Although the third option seems to be the most consistent with existing wording in the containers section of the standard, it is problematic in a number of respects. To begin with, the propagation behavior is determined at run time based on a pair of type-erased allocators, instead of at compile time. Such run-time logic is not consistent with the rest of the standard and is hard to reason about. Additionally, there are two allocator types involved, rather than one. Any set of rules that attempts to rationally interpret the propagation traits of both allocators is likely to be arcane at best, and subtly wrong for some set of codes at worst.

The second option is a non-starter. Historically, and in the vast majority of existing code, an allocator does not change after an object is constructed. The second option, if adopted, would undermine the programmer's ability to construct, e.g., an array of function objects, all using the same allocator.

The first option is (in Pablo's opinion) the simplest and best. It is consistent with historical use of allocators, is easy to understand, and requires minimal wording. It is also consistent with the wording in N3916, which formalizes type-erased allocators.

For cross-referencing purposes: The resolution of this issue should be harmonized with any resolution to LWG 2062, which questions the noexcept specification on the following member functions of std::function:

template <class F> function& operator=(reference_wrapper<F>) noexcept;
void swap(function&) noexcept;

[2015-05 Lenexa]

MC: change to P3 and status to open.

STL: note that noexcept is an issue and large chunks of allocator should be destroyed.

[2015-12-16, Daniel comments]

See 2564 for a corresponding issue addressing library fundamentals v2.

Previous resolution [SUPERSEDED]:

This wording is relative to N3936.

  1. Change 22.10.17.3 [func.wrap.func], class template function synopsis, as indicated:

    template <class A> function(allocator_arg_t, const A&) noexcept;
    template <class A> function(allocator_arg_t, const A&, nullptr_t) noexcept;
    
  2. Change 22.10.17.3.2 [func.wrap.func.con] as indicated:

    -1- When any function constructor that takes a first argument of type allocator_arg_t is invoked, the second argument shall have a type that conforms to the requirements for Allocator (Table 17.6.3.5). A copy of the allocator argument is used to allocate memory, if necessary, for the internal data structures of the constructed function object. For the remaining constructors, an instance of allocator<T>, for some suitable type T, is used to allocate memory, if necessary, for the internal data structures of the constructed function object.

    function() noexcept;
    template <class A> function(allocator_arg_t, const A&) noexcept;
    

    -2- Postconditions: !*this.

    function(nullptr_t) noexcept;
    template <class A> function(allocator_arg_t, const A&, nullptr_t) noexcept;
    

    -3- Postconditions: !*this.

    function(const function& f);
    template <class A> function(allocator_arg_t, const A& a, const function& f);
    

    -4- Postconditions: !*this if !f; otherwise, *this targets a copy of f.target().

    -5- Throws: shall not throw exceptions if f's target is a callable object passed via reference_wrapper or a function pointer. Otherwise, may throw bad_alloc or any exception thrown by the copy constructor of the stored callable object. [Note: Implementations are encouraged to avoid the use of dynamically allocated memory for small callable objects, for example, where f's target is an object holding only a pointer or reference to an object and a member function pointer. — end note]

    template <class A> function(allocator_arg_t, const A& a, const function& f);
    

    -?- Postconditions: !*this if !f; otherwise, *this targets a copy of f.target().

    function(function&& f);
    template <class A> function(allocator_arg_t, const A& a, function&& f);
    

    -6- Effects: If !f, *this has no target; otherwise, move-constructs the target of f into the target of *this, leaving f in a valid state with an unspecified value. If an allocator is not specified, the constructed function will use the same allocator as f.

    template<class F> function(F f);
    template <class F, class A> function(allocator_arg_t, const A& a, F f);
    

    -7- Requires: F shall be CopyConstructible.

    -8- Remarks: These constructors shall not participate in overload resolution unless f is Callable (20.9.11.2) for argument types ArgTypes... and return type R.

    -9- Postconditions: !*this if any of the following hold:

    • f is a null function pointer value.

    • f is a null member pointer value.

    • F is an instance of the function class template, and !f

    -10- Otherwise, *this targets a copy of f initialized with std::move(f). [Note: Implementations are encouraged to avoid the use of dynamically allocated memory for small callable objects, for example, where f's target is an object holding only a pointer or reference to an object and a member function pointer. — end note]

    -11- Throws: shall not throw exceptions when an allocator is not specified and f is a function pointer or a reference_wrapper<T> for some T. Otherwise, may throw bad_alloc or any exception thrown by F's copy or move constructor or by A's allocate function.

[2016-08 Chicago]

Tues PM: Resolved by P0302R1

Proposed resolution:

Resolved by acceptance of P0302R1.