Date: 2016-09-06
Thomas Köppe <tkoeppe@google.com>Contributors and collaborators welcome!
Allocators are perhaps one of the most pervasive, yet least loved parts of the C++ standard library. This guide attempts to explain what allocators are and how library authors are supposed to use them. On this brief journey through one of the wilder realms of C++ we will meet allocator traits, pointer traits, fancy pointers and cast operations that will shake even the strongest minds.
C++ classes often manage dynamic resources as part of their implementation. Typical examples of such classes
are dynamic containers, strings, function wrappers, file handles, mutexes, etc. There is one distinguished
resource which is needed to create new C++ objects: memory. Given appropriate memory (i.e. of the right size
and alignment) at some address p
, we can bring to life a new C++ object of type T
by
saying ::new (p) T
. This is the fundamental idea underpinning all dynamic containers.
However, the mechanism by which suitable memory is obtained is system-specific and essentially opaque to the user.
C++ offers a default mechanism, ::operator new(sizeof(T))
, but there is little to no control over
what this does. Since the logic of a dynamic container does not depend on the specifics of how memory is obtained,
general wisdom dictates that the design of container classes should be decoupled from the memory allocation policy.
Enter allocators.
For the remainder of this guide, it will be useful to have a few concrete and representative examples in mind. We pick two, which are ubiquitous and well-known. Note that the dynamic resource allocation and deallocation is entirely opaque to the user, who only ever observes the contained element values. The container owns the elements and is responsible for their storage and lifetime.
std::vector<T>
). Dynamic
memory needs to be obtained once for the entire contiguous storage of elements, and elements need to be constructed
one by one in that memory. At any given time, there may be more memory allocated than what is occupied by element
objects.std::list<T>
). For each list element, a list node needs to be
allocated dynamically, which in turn contains the element value as well as the link pointers.The C++ standard library defines the notion of an allocator, which is an object used to parametrise memory allocation and object construction. The point that an allocator has two separate responsibilities is worth repeating: an allocator knows how to obtain memory, and it knows how to construct objects.
To understand why allocators are useful, let us step back into a world without allocators. Creating a dynamic object
can be broken down into separate steps, which are typically hidden behind the convenience expressions new
and delete
:
The broken-down operations show the mental model that allocators are intended to parametrise. To achieve this,
an allocator defines four fundamental operations: allocate
, deallocate
, construct
,
and destroy
, which generalise, respectively, to the allocation function (::operator new
), the
deallocation function (::operator delete
), placement-new
, and the explicit destructor call.
To go into further detail, we need to establish some terminology. The standard reference is section [allocator.requirements].
The standard header <memory>
defines
a trait class template allocator_traits
, which provides a uniform interface to allocators. The allocator traits
reflect certain members of the actual allocator type, but they also provide defaults, so that a custom allocator only needs to
provide a minimal set of information that differs from the defaults. (The traits template may be specialised for user-defined
types, and different defaults may be provided in a specialisation.)
Suppose a
is an allocator of type Alloc
. Let us write AllocTraits = std::allocator_traits<Alloc>
.
We say that “a
is an allocator for T
”, where T = AllocTraits::value_type
. This
means that a
knows how to obtain and release memory to store objects of type T
. To do so, we call
AllocTraits::allocate(a, n)
, and we obtain storage for n
objects of type T
.
So far so good. However, we can also use a
to construct objects, either explicitly or via a default mechanism.
But not just objects of type T
,
but any type of object! This is very much intentional. Recall our example of a linked list. We would like
to be able to allocate memory for an entire list node, but we only need to construct objects of list element type. So it is
convenient if a single allocator knows how to construct objects of different types.
Here is how this works. For any pointer C * c
, and any list of arguments args...
,
we can say AllocTraits::construct(a, c, args...)
. This either invokes ::new (static_cast<void *>(c))
C(std::forward<Args>(args)...)
, or, if Alloc
defines a suitable member function, a.construct(c,
std::forward<Args>(args)...)
. In either case, the result is than an object of type C
is constructed
from the arguments at location c
, and that afterwards *c
is that object. In other words, a new object
is constructed in a given piece of raw memory. Similarly, we can call AllocTraits::destroy(a, c)
to destroy the
object. This defaults to c->~C()
, or calls a.destroy(c)
if that expression is well-formed.
So far we have dived right into object construction, but skirted the more fundamental question of how to allocate memory. Our
allocator a
for T
can only allocate memory for a contiguous sequence of objects of type T
.
To allocate memory for n
such objects, we say:
The allocator communicates memory in terms of customisable type AllocTraits::pointer
. This defaults to the
unsurprising T *
, but it may be any user-defined type. When AllocTraits::pointer
is not
the same as T *
, we say that the allocator uses a fancy pointer. Whatever the type is, it must satisfy
the concept of a NullablePointer
, which we describe below. The allocator also defines a corresponding
“const pointer” via AllocTraits::const_pointer
and “void pointers” via
AllocTraits::void_pointer
and AllocTraits::const_void_pointer
. Pointers are implicitly convertible to
const pointers. Pointers and void pointers are mutually convertible in the usual way, that is, a pointer is implicitly convertible
to a void pointer, and a void pointer can be converted explicitly to any non-void pointer (all subject to not discarding qualifiers).
However, there is no analogue for const_cast
, that is, it is not generally possible to convert a fancy pointer to a
less qualified version.
(The typical example of a fancy pointer is an “offset pointer” into shared memory that allows separate processes to access the same object in shared memory; the offset pointer stores only an offset, relative to the pointer object’s own address. This example demonstrates the complexity of the problem: The vector’s own memory must be in shared memory; each member list must allocate from the same shared memory; each string in the list must allocate from the shared memory, and all references are expressed in terms of relative offsets. However, each process must be able to obtain a proper, “normal” address for each object, even if the object is stored in shared memory, by virtue of the dereference operation.)
Now that we have convinced ourselves that pointers are not an altogether trivial matter, it is time to introduce the final
level of abstraction that the standard library has to offer: pointer traits. This is a class template, called std::pointer_traits
,
also defined in <memory>
, and among other things it allows us to obtain pointers to objects (including fancy pointers).
Let us write AllocPtrTraits = std::pointer_traits<AllocTraits::pointer>
. The pointer traits define an element
type via AllocPtrTraits::element_type
, which is either just the allocator’s value type T
if
AllocTraits::pointer == T *
, or otherwise it is reflected out of the fancy pointer type.
The important operation provided by pointer traits is obtaining a pointer to an element: AllocPtrTraits::pointer_to(r)
produces a pointer, either by computing std::addressof(r)
if AllocTraits::pointer == T *
, or otherwise
by a mechanism specified by the fancy pointer class. See below for details. Finally, it is a requirement that AllocTraits::pointer
must be dereferenceable (producing a value of type AllocTraits::value_type
) by providing operator*
and operator->
.
NullablePointer
requirementsThe type AllocTraits::pointer
may not be arbitrary, but rather it must satisfy a set of
requirements called NullablePointer
which capture the semi-regular features of the built-in
pointer types (default-constructible, copyable, copy-assignable, destructible, all without throwing exceptions;
and copying and assigning have
the usual semantics), as well as the existence of a special null value. Specifically, if a type T
satisfies the NullablePointer
requirements, then it must have a distinguished null value, which
compares equal only to itself, can be tested for by Boolean conversion, and value-initialisation of T
(i.e. T()
) produces the null value. (Therefore, the following holds: assert(!T())
.)
The fact that AllocTraits::pointer
can be any type that satisfies these requirements has
consequences for library authors: It is not generally permissible to attempt to produce a null pointer
from an initialiser such as NULL
or a literal 0
. The
pointer type may not have appropriate constructors, or the constructor overload may be ambiguous, or it
may not have the desired effect. The only way to produce a null value of type AllocTraits::pointer
is to value-initialise it or to initialize it with a value of type std::nullptr_t
.
Moreover, note that if the pointer type is a class type, then it may not be trivially constructible, since the default constructor may be required to initialise its state to the null value. Therefore, common “small-container” optimisations that represent the container class members as a union of a set of pointers and raw storage need be careful to provide the union with appropriate constructors.
In summary, it is tempting to write container code as though the pointer type were T *
,
but in order to make a container work with arbitrary standard conforming allocators, care must be taken
to talk about pointers only in the language of the NullablePointer
requirements.
We write T = AllocTraits::value_type
for brevity, and we assume that T
is
an unqualified object type.
AllocTraits::pointer
meets the NullablePointer
requirements (cf. [nullablepointer.requirements]).AllocTraits::pointer
is a user-defined class type (and not T *
), we say that Alloc
uses fancy pointers.r
of type T
, we have a “fancy address-of operator”:
AllocTraits::pointer ptr = AllocPtrTraits::pointer_to(r);
AllocTraits::pointer ptr
, we have a “fancy dereference operator”: T & r = *ptr;
This operation is only valid if the pointer is either null or dereferenceable (i.e. it cannot be one-past-the-end).
AllocTraits::const_pointer
which can be converted from
an AllocTraits::pointer
implicitly.AllocTraits::void_pointer
and AllocTraits::const_void_pointer
which
can be converted from AllocTraits::pointer
implicitly, and AllocTraits::const_void_pointer
can be converted from AllocTraits::const_pointer
and from AllocTraits::void_pointer
implicitly.
There are corresponding opposite conversions from void to non-void pointers which are explicit.Putting it all together, we arrive at a self-consistency identity for dereferenceable pointers:
Suppose now we have an allocator Alloc a;
for a type T
and we would like to
allocate memory and construct the object from some given arguments. We continue writing AllocTraits
for std::allocator_traits<Alloc>
. We need the following sequence of operations.
The full set of requirements is spread out in the C++ standard across the sections [allocator.requirements],
[allocator.traits] and [pointer.traits]. We collect a few of the essential requirements here. We continue
writing Alloc
for some, generic allocator type, and AllocTraits
and
AllocPtrTraits
for its related allocator traits and pointer traits type.
Alloc
must define a member type value_type
. Alloc
is an allocator
type “for that value”.Alloc
may define a member type pointer
, in which case AllocTraits::pointer
and AllocPtrTraits::pointer
are that same type; otherwise they are Alloc::value_type *
.Alloc
may define a member template rebind
for rebinding. This may be omitted if Alloc
is itself a template class A<T, Args...>
where the value type is the first template argument, in which case
AllocTraits::rebind_alloc<U>
defaults to A<U, Args...>
.
Alloc::pointer
is defined and a user-defined class type (as opposed to Alloc::value_type *
), then
we say that Alloc
has a fancy pointer. In that case:
Alloc::pointer
satisfies the NullablePointer
requirements.Alloc::pointer
satisfies the requirements for a random access iterator.Alloc::pointer
should have a member type element_type
(though it need
not have such a member type if it is a template class P<V, Args...>
, in which case
AllocPtrTraits::element_type
is V
).Alloc::pointer
must have a static member function pointer_to
taking a single
argument of type element_type
and returning a fancy pointer object.In all of the above we ignored the alternative possibility of specialising std::allocator_traits
directly for the desired allocator type. So rather than defining Alloc::pointer
, it is also
permissible to specialise std::allocator_traits<Alloc>
and define its member
pointer
. In that fashion, it is possible for an allocator to have fancy pointers even it if
does not define a pointer
member. (Such a design would be obscure and cumbersome, though,
since the Alloc::allocate
member function still needs to have a return type that matches
AllocTraits::pointer
.) The above requirements are to be understood with this concession in mind.
Allocators are strange animals in the C++ bestiary. The operational identity of an allocator object is not really connected
with the object itself, but rather, the identity is spread out over a whole family of related allocators. Given an allocator
a
of type Alloc
for a type T
, we can always obtain an allocator for a different type
U
like this:
We call b
a rebound copy of a
for U
.
In this construction, all that matters is that we construct one allocator from another one. This allows allocators to pass on some internal state. (However, if the allocator is empty and DefaultConstructible, then there is no distinction between constructing a rebound allocator from an original allocator and just default-constructing the rebound allocator.) The upshot is that we can obtain allocators for any type from any one given allocator, and all the allocators thus obtained are compatible in the sense that memory that was obtained with any one allocator in the family can always be deallocated with a new allocator built from any one allocator in the family.
In fact, it is required that for any two allocators a1, a2
of the same type, a1 == a2
holds whenever
memory allocated with one can be deallocated with the other. This must be true when we have Alloc a1, a2(a1);
, but
it may also be true in other cases. And there is more: For any two allocators Alloc a
and AllocTraits::rebind_alloc<U> b
, the
comparison a == b
is defined as a == AllocTraits::rebind_alloc<Alloc::value_type>(b)
.
So we can say that the identity of an allocator family lies in the shared state much more than in any one object, and
if there is no shared state (as is the case with std::allocator<T>
), then all allocators obtained by
rebinding one allocator are in the same, compatible family. Let us summarise:
Alloc
defines a family of allocator types AllocTraits::rebind_alloc<U>
for all types U
.Alloc a
defines a compatible family of allocator objects via rebound copy,
AllocTraits::rebind_alloc<U>(a)
, and every object in that family defines the same family.We can apply rebound copies repeatedly to arrive at a self-consistency identity for allocators. For any Alloc
a
and any type U
:
The standard library container requirements (cf. [container.requirements.general]) mandate that the construct
and
destroy
functions of the allocator of a container only be used to construct and destroy container elements, but
not any internal data structures. In our linked list example, this means that in order to comply with the container requirements,
the list nodes themselves may not be constructed with the allocator, only the actual data elements must be so constructed. This
may be implemented by designing the list node type so that it has a trivial constructor and destructor, so that allocated memory
may be treated directly as a list node as soon as storage for it has been obtained.
This example is worth spelling out in detail. Suppose we have elements of type T
,
list nodes of type Node<T>
, and an allocator a
for T
of type Alloc
. For example:
We need a helper function to access the storage as though it were a T
:
Now we create a new node with a value constructed from some arguments, and then destroy it right away:
(We have assumed here for simplicity that Node<T>
is trivially
constructible, so that a node’s lifetime begins as soon as storage for it is
allocated.)
Note that n_a
is a rebound copy for Node<T>
of the original allocator a
. Moreover,
we use n_a
and not a
to construct elements, even though the elements are actually of type T
,
which is the value type of a
and not of n_a
. Allocators are designed to be able to construct arbitrary
objects, not just objects of their own value type. Practically, this allows us to only keep a copy of the node allocator around,
and never store a copy of the original allocator.
Let us also study how a contiguous storage container like std::vector
should use
allocators. The situation is rather simpler than for the linked list. A vector of T
only needs one allocator a
for T
, which we continue to call Alloc
.
To obtain storage of capacity n
, we only need to call AllocTraits::allocate(a, n)
and convert the result into a true pointer, say p
, of type T *
, to the first
available slot.
Given this pointer to the start of the sequence, we can start constructing elements. Very crudely, we
can default-construct k
elements with no-throwing constructors like this:
If exceptions may occur, we also need to track our progress, catch exceptions, and in the event of an exception destroy the already constructed elements before rethrowing the caught exception.
The contiguous array is an example where the allocator fits most naturally. There is no need to rebind or to construct elements of different types. The allocator neatly separates the allocation of memory from the construction of objects.
A single dynamic container with an allocator is a fine thing, but it is only the first step. Two separate sets of questions arise naturally.
The two problems are orthogonal to one another. Both require a fair amount of machinery.
If an allocator is stateless, then all container objects that use the same allocator type have compatible allocators, and there is no problem. For example, swapping two containers can be achieved by simply exchanging internal memory handles; either container’s allocator knows how to deallocate the memory.
However, when allocators are stateful, the situation is more complicated. We distinguish three situations which require a policy decision:
X
uses an allocator of type Alloc
. Given X x1
,
which allocator does X x2(x1)
use? It could either be a copy of x.get_allocator()
, or it could
be a default-constructed, new allocator, or it could be something else. The universal answer is provided by
AllocTraits::select_on_container_copy_construction(x1.get_allocator())
. This trait is configurable,
e.g. via the member function Alloc::select_on_container_copy_construction
, and the default is to return a copy
of the existing allocator.x1 = x2
, or more interestingly yet, x1
= std::move(x2)
, you have to decide whether x1
is supposed to take the allocator from x2
or to keep its own allocator. Either choice has advantages and disadvantages in the case where the two allocators
are not equal, and so neither one can deallocate the other one’s memory. If the allocator is reassigned, then
it may be necessary to deallocate the currently held data with the old allocator first, and allocate new memory
with the newly assigned allocator, rather than keeping the existing memory and just reassigning the element values.
On the other hand, if the allocator is reassigned, move-assignment is efficient, since the entire internal allocation
can just be moved over to x1
. If the allocator is not reassigned, move-assignment would require an actual
element-by-element copy. The desired policy is chosen by means of the types AllocTraits::propagate_on_container_copy_assignment
(“POCCA”) and AllocTraits::propagate_on_container_move_assignment
(“POCMA”), which
are either std::true_type
or std::false_type
. Note that this choice is made per allocator
type, not per object.AllocTraits::propagate_on_container_swap
(“POCS”). However, the standard library container
requirements mandate that POCS is either true, or that otherwise the allocators of the containers that are to
be swapped compare equal. If this were not required, then swapping with unequal, unswapped allocators could not
preserve iterators.The allocator propagation operations are generally not allowed to throw exceptions. Specifically, if POC{CA, MA, S} is true, then {copy-assignment, move-assignment, swap} must not throw exceptions.
When an allocator is stateless, that is, when any two instances always compare equal, then it should
have POCMA be true. Otherwise, containers must perform a dynamic equality check and cannot
promise non-throwing moves. (The original std::allocator
got this wrong; see the
issues list below.)
uses_allocator
constructionSuppose we have a vector of linked lists, and the vector uses some allocator of our choice. Even if we used a list type that would also be configured to use an allocator of the same type as our vector’s allocator, we still need a mechanism for ensuring that the vector elements (which themselves are lists) are constructed with the correct allocator (namely with a suitably rebound copy of the vector allocator).
To tackle this problem, we need a standard convention how allocators are passed to class constructors. The standard library provides the following machinery.
std::uses_allocator<X, A>
, which
contains a Boolean member constant std::uses_allocator<X, A>::value
. For brevity,
let us write UsesAlloc<X> = std::uses_allocator<X, Alloc>::value
for the value
of this trait for our allocator class Alloc
. Then UsesAlloc<X>
is true
if either the trait has been thus specialised, or otherwise if X
defines a member type
X::allocator_type = Alloc
. In other words, UsesAlloc<X>
tells us whether
the class X
uses the allocator Alloc
.UsesAlloc<X>
is true
, then we assume that either
X
has a constructor X::X(std::allocator_arg_t, Alloc const &, Args...)
,
where the second parameter is the desired allocator, orX
has a constructor X::X(Args..., Alloc const &)
,
where the final parameter is the desired allocator.std::tuple
, for which the
uses_allocator
trait is specialised to true
, so that allocators may be propagated
through tuples of types which themselves require an allocator.)Alloc
, and they should use compatible allocators (i.e. their all their allocators should compare
equal). Suppose further we already have an allocator Alloc a
, and that both our vector and our
list class take the new form of constructor. Then we use a new allocator:std::allocator_traits<...>::construct(get_allocator(), p, arg1, arg2, arg2)
,
this calls get_allocator().construct(p, arg1, arg2, arg2)
on the scoped allocator adaptor. The adaptor
in turn recognises that *p
has the uses_allocator
trait, and thus it forwards the
call to std::allocator_traits<AllocTraits::rebind_alloc<ListType>>::construct(outer_allocator(),
std::allocator_arg, *this, arg1, arg2, arg3)
. In this fashion, the allocator is always rebound-copied into
the constructors of the element values, as long as those have the uses_allocator
trait set.
A few complete end-to-end examples are available on separate pages:
The complete code from the examples is available in the example_code
subdirectory
in the GitHub repository.
The specification of allocators in the C++ standard underwent significant changes from C++03 to C++11. As result, the wording that is included in C++11 contains a few points that are vague, unclear or outright impossible to implement. These issues are being addressed by the standardisation committee in library defect reports. Once a defect report is accepted, we consider it part of the language.
Here follows a partial list of issues that affect allocators.
pointer_to
. The original wording does not require that the
pointer_traits<allocator_traits<A>::pointer>::pointer_to
function exist. However,
it is generally necessary in container design. This issue adds a corresponding requirement to the allocator
requirements. The issue is still pending, but seems uncontroversial.std::allocator
should POCMA. In the original wording, std::allocator<T>::propagate_on_container_move_assignment
was std::false_type
, which meant that container moves could not be noexcept
. The accepted resolution is to
set POCMA to true for the standard allocator. This issue is accepted for C++14, but implementers have been
incorporating the resolution into C++11 libraries since without it common operations would be unacceptably
inefficient.
This guide has benefited greatly from feedback, suggestions and corrections from the C++ community. In particular, Jonathan Wakely has repeatedly provided valuable insights, and Jens Maurer helped getting the offset pointer example right.