Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the purpose of std::scoped_allocator_adaptor?

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?

like image 683
Ralph Tandetzky Avatar asked Mar 03 '14 13:03

Ralph Tandetzky


2 Answers

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.

like image 190
Jonathan Wakely Avatar answered Oct 31 '22 16:10

Jonathan Wakely


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).

like image 39
TemplateRex Avatar answered Oct 31 '22 16:10

TemplateRex