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.