3003. <future> still has type-erased allocators in promise

Section: 32.10.6 [futures.promise] Status: LEWG Submitter: Billy O'Neal III Opened: 2017-07-16 Last modified: 2024-10-02

Priority: 2

View other active issues in [futures.promise].

View all other issues in [futures.promise].

View all issues with LEWG status.

Discussion:

In Toronto Saturday afternoon LWG discussed LWG 2976 which finishes the job of removing allocator support from packaged_task. LWG confirmed that, despite the removal of packaged_task allocators "because it looks like std::function" was incorrect, they wanted to keep the allocator removals anyway, in large part due to this resolution being a response to an NB comment.

If we don't want the type erased allocator situation at all, then we should remove them from the remaining place they exist in <future>, namely, in promise.

This change also resolves potential implementation divergence on whether allocator::construct is intended to be used on elements constructed in the shared state, and allows the emplace-construction-in-future paper, P0319, to be implemented without potential problems there.

[28-Nov-2017 Mailing list discussion - set priority to P2]

Lots of people on the ML feel strongly about this; the suggestion was made that a paper would be welcomed laying out the rationale for removing allocator support here (and in other places).

[2018-1-26 issues processing telecon]

Status to 'Open'; Billy to write a paper.

[2019-06-03]

Jonathan observes that this resolution conflicts with 2095.

[Varna 2023-06-13; Change status to "LEWG"]

Previous resolution [SUPERSEDED]:

This resolution is relative to N4659.

  1. Edit 32.10.6 [futures.promise], class template promise synopsis, as indicated:

    template<class R>
    class promise {
    public:
      promise();
      template <class Allocator>
        promise(allocator_arg_t, const Allocator& a);
      […]
    };
    template <class R>
      void swap(promise<R>& x, promise<R>& y) noexcept;
    template <class R, class Alloc>
      struct uses_allocator<promise<R>, Alloc>;
    
    […]
    template <class R, class Alloc>
      struct uses_allocator<promise<R>, Alloc>
        : true_type { };
    

    -3- Requires: Alloc shall be an Allocator (16.4.4.6 [allocator.requirements]).

    promise();
    template <class Allocator>
      promise(allocator_arg_t, const Allocator& a);
    

    -4- Effects: constructs a promise object and a shared state. The second constructor uses the allocator a to allocate memory for the shared state.

[2024-09-19; Jonathan provides improved wording]

In July 2023 LEWG considered this and LWG issue 2095 and requested a new proposed resolution that kept the existing constructor (which is useful for controlling how the shared state is allocated) but removed the uses_allocator specialization that makes promise incorrectly claim to be allocator-aware. Some of the rationale in P2787R1 is applicable here too.

Without the uses_allocator specialization, there's no reason to provide an allocator-extended move constructor, resolving issue 2095.

And if we're going to continue supporting std::promise construction with an allocator, we could restore that for std::packaged_task too. That was removed by issue 2921, but issue 2976 argues that there was no good reason to do that. Removing uses_allocator for packaged_task would have made sense (as proposed below for promise) but 2921 didn't do that (which is why 2976 was needed). We can restore the packaged_task constructor that takes an allocator, and just not restore the uses_allocator specialization that implies it should be fully allocator-aware. Finally, if we restore that packaged_task constructor then we need to fix reset() as discussed in issue 2245.

In summary:

Proposed resolution:

This wording is relative to N4988.

  1. Modify 32.10.6 [futures.promise] as indicated:

    template <class R, class Alloc>
      struct uses_allocator<promise<R>, Alloc>;
    
    […]
    template <class R, class Alloc>
      struct uses_allocator<promise<R>, Alloc>
        : true_type { };
    

    -4- Preconditions: Alloc meets the Cpp17Allocator (16.4.4.6.1 [allocator.requirements.general]).

  2. Modify 32.10.10.1 [futures.task.general] as indicated:

      template<class R, class... ArgTypes>
      class packaged_task<R(ArgTypes...)> {
      public:
        // construction and destruction
        packaged_task() noexcept;
        template<class F>
          explicit packaged_task(F&& f);
        template<class F, class Allocator>
          explicit packaged_task(allocator_arg_t, const Allocator& a, F&& f);
        ~packaged_task();
    
  3. Modify 32.10.10.2 [futures.task.members] as indicated:

    template<class F>
      explicit packaged_task(F&& f);
    

    -?- Effects: Equivalent to packaged_task(allocator_arg, std::allocator<int>(), std::forward<F>(f)).

    [Drafting note: Uses of std::allocator<int> and std::allocator<unspecified> are not observable so this constructor can be implemented without delegating to the other constructor and without using std::allocator.]
    template<class F, class Allocator>
      packaged_task(allocator_arg_t, const Allocator& a, F&& f);
    

    -2- Constraints: remove_cvref_t<F> is not the same type as packaged_task<R(ArgTypes...)>.

    -3- Mandates: is_invocable_r_v<R, F&, ArgTypes...> is true.

    [Drafting note: Issue 4154 alters these Mandates: and Effects: but the two edits should combine cleanly.]

    -4- Preconditions: Invoking a copy of f behaves the same as invoking f. Allocator meets the Cpp17Allocator requirements (16.4.4.6.1 [allocator.requirements.general]).

    -5- Effects: Let A2 be allocator_traits<Allocator>::rebind_alloc<unspecified> and let a2 be an lvalue of type A2 initialized with A2(a). Creates a shared state and initializes the object's stored task with std::forward<F>(f). Uses a2 to allocate storage for the shared state and stores a copy of a2 in the shared state.

    -6- Throws: Any exceptions thrown by the copy or move constructor of f, or bad_alloc if memory for the internal data structures cannot be allocated. Any exceptions thrown by the initialization of the stored task. If storage for the shared state cannot be allocated, any exception thrown by A2::allocate.

    void reset();
    

    -26- Effects: As if Equivalent to:

    if (!valid())
      throw future_error(future_errc::no_state);
    *this = packaged_task(allocator_arg, a, std::move(f));
    
    where f is the task stored in *this and a is the allocator stored in the shared state.

    [Note 2: This constructs a new shared state for *this. The old state is abandoned (32.10.5 [futures.state]). — end note]

    -27- Throws:

    1. (27.1) — bad_alloc if memory for the new shared state cannot be allocated.
    2. (27.2) — Any exception thrown by the packaged_task constructor move constructor of the task stored in the shared state.
    3. (27.3) — future_error with an error condition of no_state if *this has no shared state.