allocator
ClassAn aspect of new
that limits its flexibility is that new
combines allocating memory with constructing object(s) in that memory. Similarly, delete
combines destruction with deallocation. Combining initialization with allocation is usually what we want when we allocate a single object. In that case, we almost certainly know the value the object should have.
When we allocate a block of memory, we often plan to construct objects in that memory as needed. In this case, we’d like to decouple memory allocation from object construction. Decoupling construction from allocation means that we can allocate memory in large chunks and pay the overhead of constructing the objects only when we actually need to create them.
In general, coupling allocation and construction can be wasteful. For example:
string *const p = new string[n]; // construct n empty strings
string s;
string *q = p; // q points to the first string
while (cin >> s && q != p + n)
*q++ = s; // assign a new value to *q
const size_t size = q - p; // remember how many strings we read
// use the array
delete[] p; // p points to an array; must remember to use delete[]
This new
expression allocates and initializes n string
s. However, we might not need n string
s; a smaller number might suffice. As a result, we may have created objects that are never used. Moreover, for those objects we do use, we immediately assign new values over the previously initialized string
s. The elements that are used are written twice: first when the elements are default initialized, and subsequently when we assign to them.
More importantly, classes that do not have default constructors cannot be dynamically allocated as an array.
allocator
ClassThe library allocator
class, which is defined in the memory
header, lets us separate allocation from construction. It provides type-aware allocation of raw, unconstructed, memory. Table 12.7 (overleaf) outlines the operations that allocator
supports. In this section, we’ll describe the allocator
operations. In § 13.5 (p. 524), we’ll see an example of how this class is typically used.
Like vector
, allocator
is a template (§ 3.3, p. 96). To define an allocator
we must specify the type of objects that a particular allocator
can allocate. When an allocator
object allocates memory, it allocates memory that is appropriately sized and aligned to hold objects of the given type:
allocator<string> alloc; // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings
This call to allocate
allocates memory for n string
s.
allocator
s Allocate Unconstructed MemoryThe memory an allocator
allocates is unconstructed. We use this memory by constructing objects in that memory. In the new library the construct
member takes a pointer and zero or more additional arguments; it constructs an element at the given location. The additional arguments are used to initialize the object being constructed. Like the arguments to make_shared
(§ 12.1.1, p. 451), these additional arguments must be valid initializers for an object of the type being constructed. In particular, if the, object is a class type, these arguments must match a constructor for that class:
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'), // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!
In earlier versions of the library, construct
took only two arguments: the pointer at which to construct an object and a value of the element type. As a result, we could only copy an element into unconstructed space, we could not use any other constructor for the element type.
It is an error to use raw memory in which an object has not been constructed:
cout << *p << endl; // ok: uses the string output operator
cout << *q << endl; // disaster: q points to unconstructed memory!
We must construct
objects in order to use memory returned by allocate
. Using unconstructed memory in other ways is undefined.
When we’re finished using the objects, we must destroy the elements we constructed, which we do by calling destroy
on each constructed element. The destroy
function takes a pointer and runs the destructor (§ 12.1.1, p. 452) on the pointed-to object:
while (q != p)
alloc.destroy(--q); // free the strings we actually allocated
At the beginning of our loop, q
points one past the last constructed element. We decrement q
before calling destroy
. Thus, on the first call to destroy, q
points to the last constructed element. We destroy
the first element in the last iteration, after which q
will equal p
and the loop ends.
Once the elements have been destroyed, we can either reuse the memory to hold other string
s or return the memory to the system. We free the memory by calling deallocate
:
alloc.deallocate(p, n);
The pointer we pass to deallocate
cannot be null; it must point to memory allocated by allocate
. Moreover, the size argument passed to deallocate
must be the same size as used in the call to allocate
that obtained the memory to which the pointer points.
As a companion to the allocator
class, the library also defines two algorithms that can construct objects in uninitialized memory. These functions, described in Table 12.8, are defined in the memory
header.
As an example, assume we have a vector
of int
s that we want to copy into dynamic memory. We’ll allocate memory for twice as many int
s as are in the vector
. We’ll construct the first half of the newly allocated memory by copying elements from the original vector
. We’ll construct elements in the second half by filling them with a given value:
// allocate twice as many elements as vi holds
auto p = alloc.allocate(vi.size() * 2);
// construct elements starting at p as copies of elements in vi
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
// initialize the remaining elements to 42
uninitialized_fill_n(q, vi.size(), 42);
Like the copy
algorithm (§ 10.2.2, p. 382), uninitialized_copy
takes three iterators. The first two denote an input sequence and the third denotes the destination into which those elements will be copied. The destination iterator passed to uninitialized_copy
must denote unconstructed memory. Unlike copy
, uninitialized_copy
constructs elements in its destination.
Like copy
, uninitialized_copy
returns its (incremented) destination iterator. Thus, a call to uninitialized_copy
returns a pointer positioned one element past the last constructed element. In this example, we store that pointer in q
, which we pass to uninitialized_fill_n
. This function, like fill_n
(§ 10.2.2, p. 380), takes a pointer to a destination, a count, and a value. It will construct the given number of objects from the given value at locations starting at the given destination.