May 30, 2024

Getting Started Writing Allocator-Aware Software

This article is a quick-start guide to writing allocator aware software. There is a related in-depth white-paper, Making C++ Software Allocator Aware. Although that white-paper is a good comprehensive reference, it can be daunting to read.

Note

Where this document seeks to quickly summarize a few important topics. The white-paper Making C++ Software Allocator Aware provides a series of detailed examples for how to make a set of increasingly complex types allocator-aware, and explains these topics in context. See Roadmap to the Making C++ Software Allocator Aware White-Paper

Why Write Allocator-Aware Software?

Allocating memory is a fundamental element of almost every significant software system. Language-supplied allocation primitives (new and delete) often provide acceptable performance, and allow for selecting a global allocation strategy (e.g., tcmalloc). However, using a well-chosen local (arena) memory allocator to provide a custom local allocation strategy can yield significant (sometimes order-of magnitude) performance improvements over relying on even the most efficient state-of-the-art, general-purpose global allocators.

For example, one could use a local memory pool for a critical long-lived data structure to ensure that memory blocks allocated for the data-structure are located in physical proximity to one another. For long-running applications, such memory locality often plays an greater role in reducing overall run time than efficient allocation/deallocation of the memory itself.

One might also use a local sequential allocator for a key short-lived data structure to ensure that memory for the data-structure is allocated and released quickly.

In addition to possible performance benefits, there are also collateral benefits to writing allocator-aware software like improved testability (see bslma::TestAllocator), and the ability to instrument and plug in custom behaviors (e.g., implementing a framework to track the memory usage of various components in a running production system).

Note

The white paper Value Proposition: Allocator-Aware (AA) Software provides an in-depth analysis of the costs and benefits of writing allocator-aware software.

Notice that for a library user, taking taking advantage of allocator-aware types is simple, and provides additional power and flexibility. However, as a library writer, providing allocator-aware types is a feature that requires additional work – work that is explained in this article.

A simple example of a user customizing the allocation behavior of a vector
// Use an efficient stack-based allocation pool for this temporary vector.
bdlma::LocalSequentialAllocator<1024> stackAllocator;
bsl::vector<Information> temp(&stackAllocator);

// ...

Behavioral Requirements for an Allocator-Aware Type

In ordered to compose allocator-aware types together and use them with common data structures like vector, an allocator-aware type must adhere to certain behaviors.

Note

The expectations of an allocator-aware type are laid out in more detail in the component documentation of bslma::UsesBslmaAllocator

The key elements are:

  • The allocator supplied at construction of an object is used for non-transient memory allocation during the object’s lifetime.

    This includes allocations performed by subobjects that themselves support the bsl allocator model (i.e., the type provides the supplied allocator to any AA subobjects).

  • The allocator used by an object does not change after construction

    The assignment operators, for example, do not assign to the allocator used by an object.

  • Transient memory allocations are generally not performed using the object’s allocator.

    I.e., allocations performed within the scope of a function where the resulting memory is deallocated before that function returns typically use the default allocator (see bslma_default).

  • The allocator used by an object is not part of the object’s value

    The allocator’s value is not considered, for example, by the equality-comparison operator operator==.

  • If an allocator is not supplied at construction, then the currently installed default allocator will typically be used

    The primary move constructor is an important exception; it uses the moved-from object’s allocator rather than the default allocator.

    See bslma_default

Creating a Basic Allocator-Aware Type

An allocator-aware type implements an interface having

  1. an allocator_type type alias,

  2. a set of constructors taking allocator arguments, and

  3. a get_allocator accessor returning the allocator provided on construction.

The following example illustrates the interface of a typical allocator-aware class.

The outline of a typical allocator-aware type
 class MyAAClass {
   public:
     // TYPES
     typedef bsl::allocator<> allocator_type;                    // allocator-aware trait

     // CREATORS
     MyAAClass();                                                // default constructor
     explicit MyAAClass(const allocator_type&);                  // extended default constructor
     MyAAClass(const MyAAClass&, const allocator_type& = {});    // extended copy constructor
     MyAAClass(MyAAClass&&) noexcept;                            // move constructor
     MyAAClass(MyAAClass&&, const allocator_type&);              // extended move constructor

     // ACCESSORS
     allocator_type get_allocator() const;                       // allocator accessor
 };

The elements of this example are described below.

Note

A complete presentation of this example is on pages 4-8 of Making C++ Software Allocator Aware

Define an Alias for bsl::allocator<>

typedef bsl::allocator<> allocator_type;

This typedef indicates that the type is allocator-aware. When allocator_type exists, the standard trait bsl::uses_allocator<MyAAClass, bsl::allocator<>>::value and the BDE trait bslma::UsesBslmaAllocator<MyAAClass>::value will both be true for MyAAClass.

Generic containers (like bsl::vector) and object-construction utilities (like bslma::ConstructionUtil) supply an allocator when constructing objects with these traits.

Add Constructors Taking an Allocator Argument

Every constructor for the class should have a variant that can be called with an allocator argument. If an allocator is not specified to a constructor, the default allocator should be used. We typically refer to an overload of a constructor taking an allocator as the “extended” version of that constructor, e.g., the “extended copy constructor”.

Note

The copy and move constructor variants taking an allocator argument are called the extended copy and extended move constructors; in this article, we refer to the variants of these constructors that do not take an allocator argument as the primary or non-extended constructors, when the context is not obvious.

An extended constructor for a simple type — a type that holds AA members but does not directly allocate memory — will simply forward the allocator to the extended constructors of its AA data-member and base-class subobjects. The allocator-aware subobjects will typically retain a copy of the allocator, making it unnecessary to dedicate a separate member variable for the allocator. Making C++ Software Allocator Aware discusses some more complicated examples (e.g., under “Implementing a Class That Allocates Memory”).

Note

Allocator-aware types do not follow the “Rule of zero”. I.e., they must explicitly define default, copy and move constructors because the compiler-generated implementations of those constructors will not reliably pass the same allocator to all subobjects

Default Constructor and Extended Default Constructor

For the default constructor, we provide separate primary and extended overloads:

An example of a default constructor and extended default constructor
MyAAClass();
explicit MyAAClass(const allocator_type& allocator);

// !NOT! MyAAClass(const allocator_type& allocator = allocator_type());

We do not use a defaulted allocator parameter for the default constructor because we want the default constructor to be implicit, but do not want the constructor taking an allocator to be implicit. Inadvertently providing an implicit conversion from an allocator is a common mistake that leads to confusing compile-time and run-time errors.

The extended default constructor should pass the allocator to the extended constructors of its allocator-aware subobjects.

Copy Constructor and Extended Copy Constructor

For the copy constructor, we typically provide a single constructor having a defaulted allocator parameter (though one may choose to provide separate primary and extended overloads).

An example of a primary/extended copy constructor
explicit MyAAClass(const MyAAClass&, allocator_type allocator = allocator_type());

Notice that allocator_type() (which is bsl::allocator<>()) returns the default allocator.

The extended copy constructor for a simple type will simply delegate to the extended copy constructors of its allocator-aware subobjects.

Move Constructor and Extended Move Constructor

For the move constructor, we provide two overloads because the primary move-constructor should be noexcept, but the extended move-construct (taking an allocator argument) cannot be noexcept. Furthermore, the allocator used for the primary move constructor should not be the default allocator but a copy of the allocator from the moved-from object.:

an example of a move constructor and extended move constructor
MyAAClass(MyAAClass&&) noexcept;
MyAAClass(MyAAClass&&, const allocator_type& allocator);

The primary move constructor for a simple type will simply delegate to the primary move constructors of its subobjects and the extended move constructor for a simple type will simply delegate to the extended move constructors of its subobjects. Making C++ Software Allocator Aware discusses some more complicated examples (e.g., under “Implementing a Class That Allocates Memory”).

Value Constructors

For each value constructor there should always be an allocator-aware (extended) variant. So, for example, if MyAAClass has a constructor taking a single int parameter, the combined primary/extended allocator would take an int parameter and an optional allocator_type parameter:

An example of a value constructor
explicit MyAAClass(int value, const allocator_type& allocator = {});

Again, the constructor should forward the allocator to its allocator-aware subobject’s constructors.

The get_allocator Accessor

The get_allocator member function returns the object’s allocator, i.e., the allocator used to construct the object. The implementation of get_allocator will typically return a copy of the allocator stored in any one of its subobjects, obviating having a redundant allocator data member.

Providing a get_allocator function is important for composition; a new class containing an allocator-aware data member named d_data can, in turn, call d_data.get_allocator() to implement its own get_allocator method (without needing an additional data member to store the allocator).

Types following the recommendations in this document will provide a get_allocator method, but many older types may still follow an older pattern where an allocator would be obtained using an allocator method. A safe and future-proof way to get an allocator from a sub-object, without worrying about the pattern it follows is to use bslma::AATypeUtil::getBslAllocator, which calls either get_allocator() or allocator(), as appropriate:

An example implementation for the get_allocator accessor
MyAAClass::allocator_type MyAAClass::get_allocator() const
{
    return bslma::AATypeUtil::getBslAllocator(d_data);
}

Passing the Allocator first Using bsl::allocator_arg_t

Typically, constructors take an optional allocator argument as their last argument (the trailing-allocator convention). For example:

explicit MyAAClass(int value, allocator_type allocator = allocator_type());

An alternative interface, however, is to specify bsl::allocator_arg_t as the first parameter of the constructor, followed by allocator_type as the second parameter (the leading-allocator convention). The leading-allocator convention is necessary when the constructor has a variadic parameter list, and is occasionally convenient for disambiguating complex overload sets (e.g., bsl::optional has variadic constructors that forward their arguments to the underlying type, and uses bsl::allocator_arg_t to indicate that an allocator is being passed).

A class having the leading-allocator convention must define the bslmf::UsesAllocatorArgT trait and use the leading-allocator convention consistently for all allocator-aware constructors. Unlike the trailing-allocator convention, the allocator parameter cannot have a default value. The only way to make the allocator optional, therefore, is to provide separate overloads for allocator-aware and non-allocator-aware constructors:

Declaring Traits and Constructors for the Leading-Allocator Convention
BSLMF_NESTED_TRAIT_DECLARATION(MyOtherAAClass, bslmf::UsesAllocatorArgT);

// Non-AA constructor
template <class... ARGS>
MyOtherAAClass(int x, ARGS&&..);

// AA constructor following the leading-allocator convention
template <class... ARGS>
MyOtherAAClass(bsl::allocator_arg_t, const allocator_type& allocator, int x, ARGS&&..);

// Error: trailing allocator convention conflicts with trait.
MyOtherAAClass(const MyOtherAAClass& original, const allocator_type& allocator = {});

Clients construct objects using the leading allocator convention by passing bsl::allocator_arg (a constant of type bsl::allocator_arg_t) and an allocator as the first two constructor arguments:

MyOtherClass value(bsl::allocator_arg, myAllocator, intArg, variadicArg1, variadicArg2 ...);

Properly written allocator-aware containers such as bsl::vector pass an allocator to their allocator-aware elements, selecting the appropriate allocator-passing convention automatically. This automatic selection is mediated by the utility components bslma::AllocatorUtil and bslma::ConstructionUtil, described below.

Passing Allocators to subobjects of a Class

The function bslma::AllocatorUtil::adapt can be used, typically in a type’s constructor, to forward an allocator to subobjects that take an allocator. The way in which classes accept allocator arguments has changed over time (from bslma::Allocator * to bsl::allocator<>) and bslma::AllocatorUtil::adapt makes the code agnostic to the allocator argument type of the contained type. This adaptation is particularly important when constructing contained objects that use the older bslma::Allocator *, as it avoids a compilation error if the type upgrades to the newer bsl::allocator<> convention.

Use bslma::AllocatorUtil::adapt to pass an allocator to a data member on construction
class MyAAType {
   bsl::string d_name;

   // ...

};

MyAAType::MyAAType(const bsl::string_view& name, ... , const allocator_type& allocator)
: d_name(name, bslma::AllocatorUtil::adapt(allocator))
{
   // ...
}

Using bslma::ConstructionUtil::make to Initialize Objects of a Template Type

When implementing a class template, bslma::ConstructionUtil::make can be used to efficiently construct an object of a generic type, and similarly will handle determining if, and how, to supply the allocator to the object being constructed.

Note

bslma::ConstructionUtil::make is only available in C++17 and later, and requires the type being initialized to support move- or copy-construction. bslalg::ConstructorProxy can be used as an alternative (e.g., in C++03), but has more awkward syntax.

Using ‘bslma::ConstructionUtil::make’ to initialize a subobject
template <class t_TYPE>
class MyAAClass {
    t_TYPE d_data;

    // ...

  public:

    explicit MyAAClass(const allocator_type& allocator)
    : d_data(bslma::ConstructionUtil::make<t_TYPE>(allocator, args...)
    {
        // Note that 't_TYPE' might, or might not, be allocator aware, and if it is
        // allocator aware, it might use the leading-allocator or trailing-allocator
        // convention.  ConstructionUtil::make will determine if and how to forward
        // the 'allocator' argument.

Allocating Memory with an Allocator

Most often, allocator-aware types simply forward their allocator to their subobjects, relying on standard library container types, like bsl::string and bsl::vector, or smart-pointer types, like bsl::shared_ptr, bsl::unique_ptr, or bslma::ManagedPtr, to perform allocations and manage memory.

Warning

Using an allocator to directly manage memory is complex and error prone and alternatives like standard containers and smart-pointers should be preferred.

Occasionally though, types have a need to directly allocate raw memory. To support this bslma provides facilities to simplify managing memory allocated directly from an allocator.

Using Smart-Pointer Types

Smart pointers can be used to manage allocated memory in a way that is less error prone than managing it directly. In addition, there are facilities, bsl::allocate_shared and bslma::ManagePtrUtil::allocateManaged, that are recommended for creating smart pointers that reduce the opportunity for mistakes when allocating a new object.

Using bsl::allocate_shared to create a bsl::shared_ptr
explicit MyAAType(allocator_type allocator)
: d_sharedPtr(bsl::allocate_shared<MyObject>(allocator, arg1, arg2, arg3, ...))
{
}
Using bslma::ManagedPtrUtil::allocateManaged to create a bslma::ManagedPtr
explicit MyAAType(allocator_type allocator)
: d_managedPtr(bslma::ManagedPtrUtil::allocateManaged<MyObject>(allocator, arg1, arg2, arg3, ...))
{
}

Note

One point that often surprises users is that neither bsl::shared_ptr nor bslma::ManagedPtr themselves follow the behavioral requirements for an allocator-aware type. Smart-pointer types are unusual because the allocator belongs to the pointed-to object, not the smart-pointer object itself. For example, a copy-constructed bsl::shared_ptr does not allocate a new object, but refers to the same object managed by the same allocator as the original object.

Allocating and Deleting Objects Directly

If one must allocate an object directly from an allocator, bslma::AllocatorUtil::newObject is the preferred mechanism for doing so. newObject will determine whether the type being created is allocator aware, and apply the correct allocator-passing convention for to the constructed object. Similarly, deleteObject can later be used to destroy the object and release its memory back to the allocator.

Using bslma::AllocatorUtil::newObject and bslma::AllocatorUtil::deleteObject to create and destroy and object
class MyAAType {
   SomeType *d_value_p;

   // ...

};

MyAAType::MyAAType(... , const allocator_type& allocator)
: d_value_p()
{
   d_value_p = bslma::AllocatorUtil::newObject<SomeType>(allocator);
}

~MyAAType()
{
   bslma::AllocatorUtil::deleteObject(get_allocator(), d_value_p);
}

Proctors and Guards

BDE provides a set of proctor and guard types to safely manage memory in the presence of exceptions. For example, a bslma::DeleteObjectProctor provides exception safety if the caller of bslma::AllocatorUtil::newObject fails to complete successfully:

Using bslma::DeleteObjectProctor to provide exception safety
MyAAClass::MyAAClass(const allocator_type& allocator)
: d_data_p(0)
{
   d_data_p = bslma::AllocatorUtil::newObject<Data>(allocator);

   bslma::DeleteObjectProctor<allocator_type, Data>
                                    delProct(allocator, d_data_p);

   // The 'DeleteObjectProctor' will destroy the allocated object and
   // release its memory if an exception occurs, preventing a memory
   // leak.

   doMoreThings();       // might throw an exception

   delProct.release();   // It is now safe to release the proctor.
}

The allocation operations on bslma::AllocatorUtil pair with proctors that can be used to provide exception safety for the results of that operation.

Proctor template

Reverses this operation

bslma::DeleteObjectProctor

bslma::AllocatorUtil::newObject

bslma::DeallocateObjectProctor

bslma::AllocatorUtil::allocateObject

bslma::DeallocateBytesProctor

bslma::AllocatorUtil::allocateBytes

Using bslma::ConstructionUtil to Construct Objects In-place

bslma::ConstructionUtil provides operations to construct objects in place at a memory address (using placement new). bslma::ConstructionUtil is used to implement bslma::AllocatorUtil::newObject, correctly determining whether a type is allocator aware, and forwarding the allocator according to the allocator-passing convention.

Using ‘bslma::ConstructionUtil::construct’ to construct an object in place
template <class t_TYPE>
class MyAAClass {
    bsls::ObjectBuffer<t_TYPE> d_data;

        // ...

      public:

        explicit MyAAClass(const allocator_type& allocator)
        {

            // Note that 't_TYPE' might, or might not, be allocator aware, and if it is
            // allocator aware, it might, or might not, use allocator_arg_t.
            // ConstructionUtil::construct will determine if and how to forward
            // the 'allocator' argument.

            bslma::ConstructionUtil::construct(d_data.address(), allocator, args...);

Roadmap to the Making C++ Software Allocator Aware White-Paper

The white-paper Making C++ Software Allocator Aware provides a series of examples for how to make a set of increasingly complex types allocator-aware. Where this article serves as a brief summary of key topics, Making C++ Software Allocator Aware serves as a reference for handling different situations.

Here is an annotated table of contents for Making C++ Software Allocator Aware.

  • Making a Simple struct Allocator-Aware (pages 8-15)

    Walks through the steps to make the most trivial type allocator-aware. Covers marking types allocator aware by defining allocator_type. Covers the default, copy, and move constructors. Also discusses passing an allocator to subobjects using bslma::AllocatorUtil.

  • Making an Attribute Class Allocator-Aware (pages 15-18)

    Expands on the simple struct example to discuss value constructors – i.e., constructors other than the default, copy, and move constructors.

  • Implementing a Class That Allocates Memory (pages 18-28)

    Goes into more detail on how to use an allocator to allocate memory (e.g., using bslma::AllocatorUtil), and how to implement operations like copy, move and swap in a way that is efficient and exception safe.

  • Implementing an AA Class Template (pages 28-33)

    Explains how to create a type where one of the data members has a template-parameter type, and where that type might or might not be allocator aware.

  • Implementing an AA Container (pages 33-37)

    Explains how to create a type that allocates memory for dynamically allocated elements of template-parameter type.

  • Pitfall: Inheriting from a bsl-AA Class (pages 37-38)

    For example creating a type that inherits from bsl::string. TL;DR, do not do this.

  • Testing AA Components (pages 38-44)

    Details on how to write unit tests for an allocator-aware type to verify its allocation behavior.