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
Table of Contents
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.
// 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
an
allocator_type
type alias,a set of constructors taking allocator arguments, and
a
get_allocator
accessor returning the allocator provided on construction.
The following example illustrates the interface of a typical allocator-aware class.
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:
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).
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.:
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:
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:
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:
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.
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.
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.
explicit MyAAType(allocator_type allocator)
: d_sharedPtr(bsl::allocate_shared<MyObject>(allocator, arg1, arg2, arg3, ...))
{
}
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.
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:
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 |
---|---|
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.
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.