3900. The allocator_arg_t overloads of generator::promise_type::operator new should not be constrained

Section: 25.8.5 [coro.generator.promise] Status: Ready Submitter: Tim Song Opened: 2023-03-04 Last modified: 2024-06-28

Priority: 3

View other active issues in [coro.generator.promise].

View all other issues in [coro.generator.promise].

View all issues with Ready status.

Discussion:

When the allocator is not type-erased, the allocator_arg_t overloads of generator::promise_type::operator new are constrained on convertible_to<const Alloc&, Allocator>. As a result, if the the allocator is default-constructible (like polymorphic_allocator is) but the user accidentally provided a wrong type (say, memory_resource& instead of memory_resource*), their code will silently fall back to using a default-constructed allocator. It would seem better to take the tag as definitive evidence of the user's intent to supply an allocator for the coroutine, and error out if the supplied allocator cannot be used.

This change does mean that the user cannot deliberately pass an incompatible allocator (preceded by an std::allocator_arg_t tag) for their own use inside the coroutine, but that sort of API seems fragile and confusing at best, since the usual case is that allocators so passed will be used by generator.

[2023-03-22; Reflector poll]

Set priority to 3 after reflector poll.

[St. Louis 2024-06-28; move to Ready]

Proposed resolution:

This wording is relative to N4928.

  1. Modify 25.8.5 [coro.generator.promise] as indicated:

    namespace std {
      template<class Ref, class V, class Allocator>
      class generator<Ref, V, Allocator>::promise_type {
      public:
        […]
        void* operator new(size_t size)
          requires same_as<Allocator, void> || default_initializable<Allocator>;
    
        template<class Alloc, class... Args>
          requires same_as<Allocator, void> || convertible_to<const Alloc&, Allocator>
            void* operator new(size_t size, allocator_arg_t, const Alloc& alloc, const Args&...);
    
        template<class This, class Alloc, class... Args>
          requires same_as<Allocator, void> || convertible_to<const Alloc&, Allocator>
            void* operator new(size_t size, const This&, allocator_arg_t, const Alloc& alloc,
                               const Args&...);
        […]
       };
    }
    
    […]
    void* operator new(size_t size)
      requires same_as<Allocator, void> || default_initializable<Allocator>;
    
    template<class Alloc, class... Args>
      requires same_as<Allocator, void> || convertible_to<const Alloc&, Allocator>
      void* operator new(size_t size, allocator_arg_t, const Alloc& alloc, const Args&...);
    
    template<class This, class Alloc, class... Args>
      requires same_as<Allocator, void> || convertible_to<const Alloc&, Allocator>
      void* operator new(size_t size, const This&, allocator_arg_t, const Alloc& alloc,
                         const Args&...);
    

    -17- Let A be

    1. (17.1) — Allocator, if it is not void,

    2. (17.2) — Alloc for the overloads with a template parameter Alloc, or

    3. (17.3) — allocator<void> otherwise.

    Let B be allocator_traits<A>::template rebind_alloc<U> where U is an unspecified type whose size and alignment are both __STDCPP_DEFAULT_NEW_ALIGNMENT__.

    -18- Mandates: allocator_traits<B>::pointer is a pointer type. For the overloads with a template parameter Alloc, same_as<Allocator, void> || convertible_to<const Alloc&, Allocator> is modeled.

    -19- Effects: Initializes an allocator b of type B with A(alloc), for the overloads with a function parameter alloc, and with A() otherwise. Uses b to allocate storage for the smallest array of U sufficient to provide storage for a coroutine state of size size, and unspecified additional state necessary to ensure that operator delete can later deallocate this memory block with an allocator equal to b.

    -20- Returns: A pointer to the allocated storage.