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).

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.

3. Proposal

We propose an optional extension to allocator to return the allocation size.

Table 34 - Cpp17Allocator Requirements

a.allocate(n)

X::pointer

Memory is allocated for n objects of type T but objects are not constructed. allocate may throw an appropriate exception. [ Note: If n==0, the return value is unspecified. — end note ]

a.allocate_with_size(n)

std::pair<X::pointer,X::size_type>

Memory is allocated for at least n objects of type T but objects are not constructed and returned as the first value. The actual number of objects m, such that m>=n, that memory has been allocated for is returned in the second value. allocate may throw an appropriate exception. [ Note: If n==0, the return value is unspecified. — end note ]

{a.allocate(n),n}

a.allocate_with_size(n,y)

std::pair<X::pointer,X::size_type>

Memory is allocated for at least n objects of type T but objects are not constructed and returned as the first value. The actual number of objects m, such that m>=n, that memory has been allocated for is returned in the second value. allocate may throw an appropriate exception. The use of y is unspecified, but is intended as a hint to aid locality. [ Note: If n==0, the return value is unspecified. — end note ]

{a.allocate(n),n}

a.deallocate(p,n)

(not used)

Requires: p shall be a value returned by an earlier call to allocate or allocate_with_size
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 match the value returned by allocate_with_size to obtain this memory.

Returns: a.allocate_with_size(n,hint) if that expression is
well-formed; otherwise, a.allocate_with_size(n) if that expression is
well-formed: otherwise, {a.allocate(n),n}.

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.