Date: 2014-07-16
Thomas Köppe <tkoeppe@google.com>Part of the Visitor’s Guide to C++ Allocators
We will develop a simple, yet non-trivial, stateful allocator which fetches memory from an existing arena, i.e. a pre-allocated chunk of memory. For this example, our arena will simply dole out memory monotonically until it is exhausted, at which point it will throw an exception.
Before we dive into allocators, let us design the arena. For the sake of illustration we include chatty messages that comment on the arena’s operation. The arena class will be unmovable and unassignable.
A few words on the details. The memory is obtained from the global allocation function
::operator new
. This is guaranteed to be maximally aligned. The arena doles
out memory of size n
and alignment a
in the allocate
function. The running offset is rounded up to the desired alignment and the address at
that offset is returned to the user; the offset is further incremented by the size of the
allocation. There is no meaningful deallocation. Had we made the allocation simpler and
always rounded up to the fixed maximal alignment (i.e. alignof(std::max_align_t)
),
we could conceivably have supported shrinking the arena from the back, but this is not
important. The allocate
function throws an exception if there is no more
space.
We now design a stateful allocator that obtains its memory from an arena. Thanks to the
defaults provided by std::allocator_traits
, we only need to fill in a minimal
set of information.
The allocator’s identity is the value of its arena
member. The identity
is preserved by rebound-copies and verified with the equality and inequality operators.
The only non-trivial operation of this allocator is to forward allocation calls to the
arena (computing the desired type’s alignment in the process).
First, let us make a few arenas and some prototype allocators.
Now we will not actually be using the allocator directly, but always wrap it in a scoped adaptor. A type alias is handy.
Now we are ready to allocate an actual data structure from our arena. As an example (which is fairly plausible in real applications), we consider a map with string keys. Both the map itself and the keys should be allocated from the same arena.
This example will make use of library features introduced in C++14, for subtle reasons which we will point out below. This does not constrain the general validity of the example, though, which is otherwise compliant with C++11.
The first step when making containers of elements which themselves use allocators is to give each nested type the same scoped allocator type. We define several type aliases for this purpose.
Both the map type itself and the contained string type are using the scoped allocator
SA
that we defined above. Spelling out the map’s value type is tedious,
but it draws attention to an interesting point we will making shortly.
Note the comparator type std::less<>
. This is a feature from C++14, called
a transparent comparator. The call operator of the default std::less<astring>
expects two actual astring
arguments, so we could only ever search for keys that
are arena-allocated strings. (Every map search would deplete our arena!) By contrast, the call
operator of a transparent comparator is a template which accepts any type and forwards
it to the comparison operator. To take advantage of this mechanism, the map’s find
and erase
members must themselves be templates, which was added in C++14. We will
see shortly how to take advantage of the transparent comparator.
Finally we can create, populate and explore our strings and maps.
This code highlights several interesting aspects of allocator-aware code.
pointer
s or one pointer
and two size_type
s
long). We are using long strings to ensure that our allocator is actually being used.b
. The map’s internal allocator will use the
construct()
function of the allocator traits, forwarding the string rvalue.
But the scoped allocator will mangle this construction call and insert its held allocator
argument, because the string satisfies the uses_allocator
trait, so that the
actual string constructor that is being called is basic_string(basic_string && rhs,
Alloc const & a)
. That constructor must check whether the allocator of rhs
and a
are equal. If not, then the two allocators cannot free each other&rsquo's memory,
and the constructor has to actually copy the string data rather than only copying the pointers.value_type
, which is a pair, and pairs do not take allocators.
However, scoped_allocator_adaptor::construct
actually contains a special provision for pair arguments which will apply the uses-allocator
construction to each pair element individually. By contrast, there is no such special provision
for std::tuple
, but instead, uses_allocator
is specialised for tuples
to be true
, and std::tuple
contains a set of allocator-extended
constructors, so that the same design can be applied to containers of tuples.find
call uses the transparent comparator. Rather than searching for an allocated
astring
, the char const *
argument is forwarded directly to the comparison
expression, which in turn allows overload resolution to find the operator<
for
basic_string
that compares a string instance with a null-terminated character array. No
second string is ever constructed. This design works well enough for our example, but note that we cannot
compare std::basic_string
s with different allocator types, since basic_string
does
not contain overloaded comparison operator templates parametrised on the allocator. However, it is perfectly
feasible to write a custom, transparent comparator (parametrised only on the character traits) that compares
a larger set of string-like objects (basic_string
s, string views, character arrays).Finally, here is the annotated and slightly edited output of a typical run (on a 64-bit system with short-string optimisation).