Background
The first version of allocators in C++ defined the named requirement
Allocator, and made each standard container a class template
parameterized on the allocator type. For example, here is the declaration for
std::vector
:
namespace std {
template< class T, class Allocator = std::allocator< T > >
class vector;
} // namespace std
The standard allocator is DefaultConstructible. To support stateful allocators, containers provide additional constructor overloads taking an allocator instance parameter.
namespace std {
template< class T, class Allocator >
class vector
{
public:
explicit vector( Allocator const& alloc );
//...
While the system works, it has some usability problems:
-
The container must be a class template.
-
Parameterizing the allocator on the element type is clumsy.
-
The system of allocator traits, especially POCCA and POCMA, is complicated and error-prone.
Allocator-based programs which use multiple allocator types incur a greater number of function template instantiations and are generally slower to compile because class template function definitions must be visible at all call sites.
Polymorphic Allocators
C++17 improves the allocator model by representing the low-level allocation
operation with an abstract interface called memory_resource
, which is not
parameterized on the element type, and has no traits:
namespace std {
namespace pmr {
class memory_resource
{
public:
virtual ~memory_resource();
void* allocate ( size_t bytes, size_t alignment );
void deallocate( void* p, size_t bytes, size_t alignment );
bool is_equal ( const memory_resource& other ) const;
protected:
virtual void* do_allocate ( size_t bytes, size_t alignment ) = 0;
virtual void do_deallocate( void* p, size_t bytes, size_t alignment ) = 0;
virtual bool do_is_equal ( memory_resource const& other ) const noexcept = 0;
};
} // namespace pmr
} // namespace std
The class template polymorphic_allocator
wraps a memory_resource
pointer and meets the requirements of Allocator, allowing it to be used
where an allocator is expected. The standard provides type aliases using the
polymorphic allocator for standard containers:
namespace std {
namespace pmr {
template< class T >
using vector = std::vector< T, boost::container::pmr::polymorphic_allocator< T > >;
} // namespace pmr
} // namespace std
A polymorphic allocator constructs with a pointer to a memory resource:
// A type of memory resource
monotonic_resource mr;
// Construct a vector using the monotonic buffer resource
vector< T > v1(( boost::container::pmr::polymorphic_allocator< T >(&mr) ));
// Or this way, since construction from memory_resource* is implicit:
vector< T > v2( &mr );
The memory resource is passed by pointer; ownership is not transferred. The caller is responsible for extending the lifetime of the memory resource until the last container which is using it goes out of scope, otherwise the behavior is undefined. Sometimes this is the correct model, such as in this example which uses a monotonic resource constructed from a local stack buffer:
{
// A type of memory resource which uses a stack buffer
unsigned char temp[4096];
static_resource mr( temp, sizeof(temp) );
// Construct a vector using the static buffer resource
vector< value > v( &mr );
// The vector will allocate from `temp` first, and then the heap.
}
However, sometimes shared ownership is needed. Specifically, that the lifetime extension of the memory resource should be automatic. For example, if a library wants to return a container which owns an instance of the library’s custom memory resource as shown below:
namespace my_library {
std::pmr::vector<char> get_chars1()
{
// This leaks memory because `v` does not own the memory resource
std::pmr::vector<char> v( new my_resource );
return v;
}
} // my_library
This can be worked around by declaring the container to use a custom allocator
(perhaps using a std::shared_ptr< std::pmr::memory_resource >
as a data
member). This hinders library composition; every library now exports unique,
incompatible container types. A raw memory resource pointer is also easy to
misuse:
namespace my_library {
std::pmr::vector<char> get_chars2()
{
// Declare a local memory resource
my_resource mr;
// Construct a vector that uses our resource
std::pmr::vector<char> v( &mr );
// Undefined behavior, `mr` goes out of scope!
return v;
}
} // my_library
Workarounds for this problem are worse than the problem itself. The library
could return a pair with the vector and
std::unique_ptr<std::pmr::memory_resource>
which the caller must manage. Or
the library could change its function signatures to accept
a memory_resource
*
provided by the caller, where the library also
makes public the desired memory resources (my_resource
above).
Problem Statement
We would like an allocator model using a single type T
with the
following properties:
-
T
is not a class template -
T
references amemory_resource
-
T
supports both reference semantics or shared ownership -
T
interoperates with code already usingstd::pmr
Boost.JSON solves this problem by introducing a new smart pointer called
storage_ptr
which builds upon C++17’s memory allocation interfaces,
accomplishing the goals above. As a result, libraries which use this type
compose more easily and enjoy faster compilation, as member functions for
containers which use the type can be defined out-of-line.