In the C++11 standard we have std::scoped_allocator_adaptor
in the dynamic memory management library. What are the most important use cases of this class?
If you want a container of strings and want to use the same allocator for the container and its elements (so they are all allocated in the same arena, as TemplateRex describes) then you can do that manually:
template<typename T>
using Allocator = SomeFancyAllocator<T>;
using String = std::basic_string<char, std::char_traits<char>, Allocator<char>>;
using Vector = std::vector<String, Allocator<String>>;
Allocator<String> as( some_memory_resource );
Allocator<char> ac(as);
Vector v(as);
v.push_back( String("hello", ac) );
v.push_back( String("world", ac) );
However, this is awkward and error-prone, because it's too easy to accidentally insert a string which doesn't use the same allocator:
v.push_back( String("oops, not using same memory resource") );
The purpose of std::scoped_allocator_adaptor
is to automatically propagate an allocator to the objects it constructs if they support construction with an allocator. So the code above would become:
template<typename T>
using Allocator = SomeFancyAllocator<T>;
using String = std::basic_string<char, std::char_traits<char>, Allocator<char>>;
using Vector = std::vector<String, std::scoped_allocator_adaptor<Allocator<String>>>;
/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
Allocator<String> as( some_memory_resource );
Allocator<char> ac(as);
Vector v(as);
v.push_back( String("hello") ); // no allocator argument needed!
v.push_back( String("world") ); // no allocator argument needed!
Now the vector's allocator is automatically used to construct its elements, even though the objects being inserted, String("hello")
and String("world")
, are not constructed with the same allocator. Since basic_string
can be implicitly constructed from const char*
the last two lines can be simplified even further:
v.push_back( "hello" );
v.push_back( "world" );
This is much simpler, easier to read, and less error-prone, thanks to scoped_allocator_adaptor
constructing the elements with the vector's allocator automatically..
When the vector asks its allocator to construct an element as a copy of obj
it calls:
std::allocator_traits<allocator_type>::construct( get_allocator(), void_ptr, obj );
Normally the allocator's construct()
member would then call something like:
::new (void_ptr) value_type(obj);
But if the allocator_type
is scoped_allocator_adaptor<A>
then it uses template metaprogramming to detect whether value_type
can be constructed with an allocator of the adapted type. If value_type
doesn't use allocators in its constructors then the adaptor does:
std::allocator_traits<outer_allocator_type>::construct(outer_allocator(), void_ptr, obj);
And that will call the nested allocator's construct()
member, which uses something like placement new, as above. But if the object does support taking an allocator in its constructor then the scoped_allocator_adaptor<A>::construct()
does either:
std::allocator_traits<outer_allocator_type>::construct(outer_allocator(), void_ptr, obj, inner_allocator());
or:
std::allocator_traits<outer_allocator_type>::construct(outer_allocator(), void_ptr, std::allocator_arg, inner_allocator(), obj);
i.e. the adaptor passes additional arguments when it calls construct()
on its nested allocator, so that the object will be constructed with the allocator. The inner_allocator_type
is another specialization of scoped_allocator_adaptor
, so if the element type is also a container, it uses the same protocol to construct its elements, and the allocator can get passed down to every element, even when you have containers of containers of containers etc.
So the purpose of the adaptor is to wrap an existing allocator and perform all the metaprogramming and manipulation of constructor arguments to propagate allocators from a container to its children.
Say you have a stateful arena allocator Alloc
with a constructor Alloc(Arena&)
that allows some special performance for your application, and say that you use a nested hierarchy of containers like this:
using InnerCont = std::vector<int, Alloc<int>>;
using OuterCont = std::vector<InnerCont, std::scoped_allocator_adaptor<Alloc<InnerCont>>>;
Here, the use of scoped_allocator_adaptor
will let you propagate the arena object used to initialize your allocator from the outer to the inner container like this:
auto my_cont = OuterCont{std::scoped_allocator_adaptor(Alloc<InnerCont>{my_arena})};
This achieve greater data locality and lets you pre-allocate one big memory arena my_arena
for your entire container hierarchy, rather than only make my_arena
available for the outer container, and requiring a loop over all innner containers with another arena for each element at that level.
The class template is actually a variadic template that gives you fine-grained control about which type of allocator to use in each type of the container hierarchy. Presumably this gives complicated data structures better performance (I must confess I haven't seem different allocators in different levels in action anywhere, but maybe large data centers with multi-million users have a use case here).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With