1. Introduction
This is a library paper to describe how size feedback can be used with allocators:
-
In the case of
, the language feature proposed in [P0901R3] could be used.std :: allocator -
In the case of custom allocators, the availability of the traits interface allows standard library containers to leverage this functionality.
2. Motivation
Consider code adding elements to
:
std :: vector < int > v = { 1 , 2 , 3 }; // Expected: v.capacity() == 3 // Add an additional element, triggering a reallocation. v . push_back ( 4 );
Many allocators only allocate in fixed-sized chunks of memory, rounding up
requests. Our underlying heap allocator received a request for 12 bytes (
) while constructing
. For several implementations, this request
is turned into a 16 byte region.
2.1. Why not realloc
poses several problems problems:
-
We have unclear strategies for growing a container. Consider the two options (
anda
) 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.b std :: vector < int > v = { 1 , 2 , 3 }; // Expected: v.capacity() == 3 // Add an additional element. Does the vector: // a) realloc(sizeof(int) * 4), adding a single element // b) realloc(sizeof(int) * 6), attempt to double, needing reallocation v . push_back ( 4 ); // Add another element. // a) realloc(sizeof(int) * 5), fails in-place, requires reallocation // b) size + 1 <= capacity, no reallocation required v . push_back ( 5 ); Option
requires every append (after the initial allocation) try toa
, requiring an interaction with the allocator each time.realloc Option
requires guesswork to probe the allocator’s true size. By doubling (the typical growth pattern forb
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 tostd :: vector
), as well as copies (when we cannot resize in-place).realloc 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
'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 underlyingmemcpy
implementations (libc, in the case of libc++ and libstdc++) used to buildmalloc
, nor could it interact with user-provided replacement functions.operator new In-place reallocation has been considered in [N3495] (throwing
when unable) and [P0894R1] (returningstd :: bad_alloc 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
Wording relative to [N4800].
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 objects of type
n but objects are not constructed.
T may throw an appropriate exception. [ Note: If
allocate , the return value is unspecified. — end note ]
n == 0
a . allocate_with_size ( n )
std :: pair < X :: pointer , X :: size_type > Memory is allocated for at least objects of type
n but objects are not constructed and returned as the first value. The actual number of objects
T , such that
m , that memory has been allocated for is returned in the second value.
m >= n may throw an appropriate exception. [ Note: If
allocate , the return value is unspecified. — end note ]
n == 0
{ a . allocate ( n ), n }
a . allocate_with_size ( n , y )
std :: pair < X :: pointer , X :: size_type > Memory is allocated for at least objects of type
n but objects are not constructed and returned as the first value. The actual number of objects
T , such that
m , that memory has been allocated for is returned in the second value.
m >= n may throw an appropriate exception. The use of
allocate is unspecified, but is intended as a hint to aid locality. [ Note: If
y , the return value is unspecified. — end note ]
n == 0
{ a . allocate ( n ), n }
a . deallocate ( p , n ) (not used) Requires: shall be a value returned by an earlier call to
p or
allocate that has not been invalidated by an intervening call to
allocate_with_size . If this memory was obtained by a call to
deallocate ,
allocate shall match the value passed to
n to obtain this memory. Otherwise,
allocate shall match the value returned by
n to obtain this memory.
allocate_with_size Throws: Nothing.
Amend [allocator.traits]:
namespace std { template struct allocator_traits { ... [[ nodiscard ]] static pointer allocate ( Alloc & a , size_type n ); [[ nodiscard ]] static pointer allocate ( Alloc & a , size_type n , const_void_pointer hint ); [[ nodiscard ]] static std :: pair allocate_with_size ( Alloc & a , size_type n ); [[ nodiscard ]] static std :: pair allocate_with_size ( Alloc & a , size_type n , const_void_pointer hint ); ... }; }
Amend [allocator.traits.members]:
[[ nodiscard ]] static pointer allocate ( Alloc & a , size_type n );
Returns:
a . allocate ( n ) [[ nodiscard ]] static pointer allocate ( Alloc & a , size_type n , const_void_pointer hint );
Returns:
if that expression is well-formed; otherwise,
a . allocate ( n , hint ) .
a . allocate ( n ) [[ nodiscard ]] static std :: pair allocate_with_size ( Alloc & a , size_type n );
Returns:
if that expression is well-formed, otherwise,
a . allocate_with_size ( n )
{ a . allocate ( n ), n } [[ nodiscard ]] static pointer allocate ( Alloc & a , size_type n , const_void_pointer hint );
Returns:
if that expression is well-formed; otherwise,
a . allocate_with_size ( n , hint ) if that expression is well-formed: otherwise,
a . allocate_with_size ( n ) .
{ a . allocate ( n ), n }
4. Design Considerations
4.1. allocate
selection
There are multiple approaches here:
-
Return a pointer-size pair, as presented.
-
Overload
and return via a reference parameter. This potentially hampers optimization depending on ABI, as the value is returned via memory rather than a register.allocate
4.2. deallocate
changes
We now require flexibility on the size we pass to
. 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
.
As the true size is expected to be useful for the
of a
or
instance, the returned size is available without additional storage
requirements. The original (requested) size is unavailable, so we relax the
size parameter requirements for these allocations.
4.3. Interaction with polymorphic_allocator
is implemented using virtual functions. Adding new
methods, such as the proposed allocate API would require taking an ABI break.