Introduction

The STL allocator is one of the most overlooked topic in most C++ teachings. They are rarely used explicitly, either via direct client code, or direct construction of an allocator to be used in a container. The possible, and perhaps only area, where you might even notice the word allocator is when you make use of the STL container classes, and wonder what that last parameter (Allocator) actually is.

In this article I will explain the purpose of the allocator, what qualifies as a Standard C++ allocator, how an allocator can be implemented, as well as possible usages and extensions.

The C++ standard purpose

Described in [Josuttis 1999],

....Allocators originally were introduced as part of the STL to handle the nasty problem of different pointer types on PCs (such as near, far, and huge pointers). They now serve as a base for technical solutions that use certain memory models, such as shared memory, garbage collection, and object-oriented databases, without changing the interfaces. However, this use is relatively new and not yet widely adopted ....Allocators represent a special memory model and are an abstraction used to translate the need to use memory into a raw call for memory. They provide an interface to allocate, create, destroy, and deallocate objects. With allocators, containers and algorithms can be parameterized by the way the elements are stored. For example, you could implement allocators that use shared memory or that map the elements to a persistent database...

There is, indeed, very little information in [C++] on allocators. It all boils down to simply two sections: 20.1.5 Allocator requirements [lib.allocator.requirements], and 20.4.1 The default allocator [lib.default.allocator]. In fact, the most important part one should take note is in fact, 20.1.5.1,

The library describes a standard set of requirements for allocators, which are objects that encapsulate the information about an allocation model. This information includes the knowledge of pointer types, the type of their difference, the type of the size of objects in this allocation model, as well as the memory allocation and deallocation primitives for it. All of the containers (_lib.containers_) are parameterized in terms of allocators.

The provided std::allocator in 20.4.1 is the only predefined and required allocator imposed by [C++] on all C++ compiler implementations.

The C++ standard definition

The standards require the allocator to define types of pointer to T(pointer), pointer to constant T(const_pointer), reference to T(reference), reference to constant T, type of T itself (value_type), an unsigned integral type that can represent the size of the largest object in the allocation model (size_type), as well as a signed integral type that can represent the difference between any two pointers in the allocation model (difference_type).

The standards then require a template class rebind member, which should pay heed of the following paragraph from 20.1.5.3

The template class member rebind in the table above is effectively a template typedef: if the name Allocator is bound to SomeAllocator<T>, then Allocator::rebind<U>::other is the same type as SomeAllocator<U>.

In short, given allocator<T>, we can simply do allocator::rebind<U>::other.allocate(1) to be allocating memory large enough to hold an object U. This is the magic required for std::list to work properly, since given std::list<int>(allocator<int>()), std::list actually needs to allocate memory for Node<int>, and not int. Thus, they need to rebind to allocator<int>()::rebind<Node<int> >::other instead.

Next, we have to provide a function to simply return the address of a given object (address).

What follows is the heart of the allocator, a function to allocate memory for n objects of type T but not construct the objects (allocate(n,u), where u is a hint for other memory models), as well a function to deallocate n objects of type T (deallocate(p, n)). Objects must be destroyed prior to this call.

As mentioned, allocate and deallocate are simply low level memory management and do not play a part in object construction and destruction. This would mean that the default usage of the keywords new and delete would not apply in these functions. As any intermediate C++ programmer should know, the following code.

The purpose of the allocator is to 2allocate raw memory without construction of objects, as well as simply deallocate memory without the need to destroy them, hence the usage of operatornew and operatordelete directly is preferred over the usage of the keywords new and delete.

Following these are helper functions to do copy construction (construct(p, t)) and destroy (destroy(p)) an object, as well as getting the largest value that can meaningfully be passed to allocate (max_size), copy constructor and default constructor, and the equality checking operators(== and !=).

A sample allocator

Below is a definition as well as implementation of an allocator that conforms to the C++ standards.

This is the basic implementation of an allocator, which should be similar to most std::allocator provided by your compiler's vendor. However, to the experienced reader, one could immediately identify a possible error. &r could only work if T did not provide an overloaded operator &, and if T should, must return the address of the object. Otherwise, the user cannot have a reliable means of obtaining the address of an object T.

Decomposing allocator into policies and traits3

Addressing the above address issue (pun intended), it is only logical that we should allow the creator of T to actually provide us with the means of obtaining the right address of object T. Following a similar design of STL, we could come up with a Trait class to provide such a means. As we identified logical designs that should be lifted into a Trait class, we should come up with the following design.

As noted, we have construct/destroy in as well, since the way the object are constructed/destroyed should be defined by the trait of object T. However, take note that construct/destroy are not always called upon after memory allocation by the containers, nor before memory deallocation by the containers. This is because the containers do not construct objects, they construct copies of objects2. Thus, construct/destroy will not be a reliable means to check for object construction/destruction.

With the ObjectTraits, should a creator of T decide to construct, destroy, or overload operator &, he could do a complete template specialization of ObjectTraits for his purpose.

Following traits, we can also abstract the actual memory allocation/deallocation code into a policy itself.

The allocation policy determines how the memory allocation/deallocation works, the maximum number of objects of type T that can be allocated, as well as the equality checks to determine if other allocators can allocate and deallocate between allocators interchangeably.

With a simple trait and allocation policy completed, we can now build upon an extensible allocator interface.

Notice the usage of public inheritance of policy and traits, as opposed to having them as a member data. This enables each allocator instance to have his own memory management model (via the policy), as well as take advantage of EBCO (Empty Base Class Optimization) if available by the compiler, since in most cases, traits would be an empty class.

The Allocator class usage is as simple as follows,

std::vector<int, Allocator<int> > v;

Memory allocation tracking policy implementation

The previous listed allocator does the most basic memory management. Building on a working model, we could actually perform profiling on memory management, for example, by having the following memory allocation tracking policy,

This allocation policy merely adds tracking capability, and is built upon another/actual memory allocation policy, determined by the second template argument.

The class usage is as simple as follows,

std::vector<int, Allocator<int, TrackAllocPolicy<int> > > v;

Small Object Allocator

Another possible implementation of an allocation policy might be optimization for small objects allocation. Frequent allocation/deallocation of small objects from the free store can, at times, hurt the performance of an application. A work-around for this is to allocate a block of large memory at one time, and hand out these memory to the application upon request. Deallocated small objects are returned to the block, and reused at a later date.

Since Loki5 has a Small Object Allocator class done, it would be logical for us to adapt it as our allocator.

If you had looked into the Loki's SmallObjAllocator, you would note that it works best for single allocation of a fixed size (for allocation of different sizes, they actually create different internal allocators). Thus, it would not work well with std::vector or other similar containers that does allocation of a block of memory (and of varied size).

Of course, if before hand, you knew the maximum size of your structure, you could do the following as well,

std::vector<int> v;
v.reserve(10);

Alternatively, if you want to do away with dynamic memory allocation, you could declare an allocator that provides a fixed-sized array, though with the way std::vector takes in an allocator as its constructor, you would end up with an unnecessary temporary. (Nevertheless, the codes are provided in the source, and it only works if and only if you know the size before hand, and have used the reserve function)

Resolving multi-threading issues

Initially I had wanted to have custom threading policies in the Allocator class, because, for example, there's SmallObjectAllocator, which used a shared memory pool for giving out objects T. Multiple thread using SmallObjectAllocator<T, N> might actually cause inconsistent state of the keeping track of internal memory pool. Except that a mutex/lock of any form didn't work out in the Allocator (Thanks to Sean Kent for pointing that out), and even in SmallObjectAllocator<T, N>, since it is still different instances, but sharing the same static Loki::SmallObjAllocator. The only possible solution would be to actually perform the lock/mutex within a wrapper for Loki::SmallObjAllocator, or even Loki::SmallObjAllocator itself.

Thus, this implementation is actually not thread-safe by itself (at least in the case of SmallObjectAllocator).

Conclusion

The allocator concept is a very powerful method to encapsulate your memory management model, and it does not touch on the overloading operator new and delete, which some see it as bad form or evil. With allocators, you can plug memory tracking, optimized memory management model for certain objects, etc into your containers. If you designed and built your framework around allocators, the possibilities are immerse.

Of course, do not go into the "Golden hammer" syndrome, or what some would describe as "When you have a hammer, everything else looks like a nail". There are certain designs where plugging in an allocator would not be logical, and although not much harm can be done (other than perhaps build times and dependency), it offers not much benefit as well.

Footnotes

1 Described in [Lippman 1996], Chp 5, Semantics of Construction, and Copy.

2 In STL Containers, there is no need for construction of objects via the default constructor, because the data in the container are merely a copy created from the actual data inserted (using the copy constructor).

3 Credits and recognition should be given to Sean Kent for the original idea and implementation of the policy-based allocator. Note that there are similarities and differences between the version described here and his original version.

History

19th August 2003: Initial version uploaded.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.