2. Motivation

Many allocators only allocate in fixed-sized chunks of memory, rounding up
requests. Our underlying heap allocator received a request for 12 bytes (3*sizeof(int)) while constructing v. For several implementations, this request
is turned into a 16 byte region.

2.1. Why not realloc

realloc poses several problems problems:

We have unclear strategies for growing a container. Consider the two
options (a and b) outlined in the comments of the example below.
Assume, as with the §2 Motivation example, that there is a 16 byte size
class for our underlying allocator.

Option a requires every append (after the initial allocation) try to realloc, requiring an interaction with the allocator each time.

Option b requires guesswork to probe the allocator’s true size. By
doubling (the typical growth pattern for std::vector in libc++ and
libstdc++), we skip past the existing allocation’s size boundary. While we
can use a reduced growth rate, doing so requires more interactions with the
allocator (calls to realloc), as well as copies (when we cannot resize
in-place).

Both of these options add extra branching on the control flow for growing
the buffer.

When the allocator cannot resize the buffer in-place, it allocates new
memory and memcpy's the contents. For non-trivially copyable types, this
is disastrous. The language lacks support for reallocating in-place without moving. While this function could be implemented as magic
library function, it could not interact with many underlying malloc implementations (libc, in the case of libc++ and libstdc++) used to build operatornew, nor could it interact with user-provided replacement
functions.

In-place reallocation has been considered in [N3495] (throwing std::bad_alloc when unable) and [P0894R1] (returning false when
unable). This has sometimes been referred to as an "extend" or an "expand"
interface. Extending an existing allocation requires a round-trip to the
allocator (and its requisite branch/call overheads) to determine whether
the allocation could be expanded. As discussed previously, we need to
probe for the true size of the backing object (until we find extend fails)
until we give up and apply a multiplicative expansion factor.

For fixed-size allocators it makes more sense, and is much simpler for
containers to adapt to, if the allocator is able to over-allocate on the
initial request and inform the caller how much memory was made available.

2.2. Why not ask the allocator for the size

We could also explore APIs that answer the question: "If I ask for N bytes,
how many do I actually get?" [jemalloc] calls this nallocx.

nallocx requires an extra call / size calculation. This requires we
duplicate work, as the size calculation is performed as part of allocation
itself.

nallocx impairs telemetry for monitoring allocation request sizes. If a nallocx return value is cached, the user appears to be asking for exactly
as many bytes as nallocx said they would get.

See also [P0901R5]'s discussion "nallocx: not as awesome as it looks."

Effects: Let s be the result of allocate_at_least(a,n).
Memory is allocated for at least n objects of type T but objects are not
constructed.

Returns:sized_ptr_t{ptr,m}, where ptr is that memory and m is the number of objects for which memory has been allocated, such that m>=n.

[ Note: If s.n==0, the value of s.ptr is unspecified. —end note]

Table 34 - Cpp17Allocator Requirements

a.deallocate(p,n)

(not used)

Requires: p shall be a value returned by an earlier call to allocate or allocate_at_least
that has not been invalidated by
an intervening call to deallocate.
If this memory was obtained by a call
to allocate,n shall match the value passed to allocate to obtain
this memory.
Otherwise, n shall satisfy capacity>=n>=requested where [p,capacity]=allocate_at_least(requested) was used to obtain this memory.

Throws: Nothing.

4. Design Considerations

4.1. allocate selection

There are multiple approaches here:

Return a pointer-size pair, as presented.

Overload allocate and return via a reference parameter. This potentially
hampers optimization depending on ABI, as the value is returned via memory
rather than a register.

4.2. deallocate changes

We now require flexibility on the size we pass to deallocate. For container
types using this allocator interface, they are faced with the choice of storing both the original size request as well as the provided size (requiring
additional auxillary storage), or being unable to reproduce one during the call
to deallocate.

As the true size is expected to be useful for the capacity of a string or vector instance, the returned size is available without additional storage
requirements. The original (requested) size is unavailable, so we relax the deallocate size parameter requirements for these allocations.

4.3. Interaction with polymorphic_allocator

std::pmr::memory_resource is implemented using virtual functions. Adding new
methods, such as the proposed allocate API would require taking an ABI break.