Ответ 1
Если вам нужен контейнер строк и вы хотите использовать один и тот же распределитель для контейнера и его элементов (так что все они выделены на той же арене, как описано в TemplateRex), вы можете сделать это вручную:
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) );
Однако это неудобно и подвержено ошибкам, потому что слишком легко случайно вставить строку, которая не использует один и тот же распределитель:
v.push_back( String("oops, not using same memory resource") );
Цель std::scoped_allocator_adaptor
- автоматически распространять распределитель на объекты, которые он строит, если они поддерживают построение с помощью распределителя. Таким образом, код выше:
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!
Теперь векторный распределитель автоматически используется для построения своих элементов, даже если вставляемые объекты String("hello")
и String("world")
не строятся с одним и тем же распределителем. Поскольку basic_string
может быть неявно построено из const char*
, последние две строки могут быть еще более упрощены:
v.push_back( "hello" );
v.push_back( "world" );
Это намного проще, легче читать и меньше подвергать ошибкам, благодаря scoped_allocator_adaptor
автоматически создавать элементы с векторным распределителем.
Когда вектор просит свой распределитель построить элемент как копию obj
, он вызывает:
std::allocator_traits<allocator_type>::construct( get_allocator(), void_ptr, obj );
Обычно член-распределитель construct()
затем вызывал бы что-то вроде:
::new (void_ptr) value_type(obj);
Но если allocator_type
- scoped_allocator_adaptor<A>
, то он использует метапрограммирование шаблонов, чтобы определить, можно ли построить value_type
с помощью распределителя адаптированного типа. Если value_type
не использует распределители в своих конструкторах, то адаптер выполняет:
std::allocator_traits<outer_allocator_type>::construct(outer_allocator(), void_ptr, obj);
И это вызовет член вложенного распределителя construct()
, который использует что-то вроде размещения new, как указано выше. Но если объект поддерживает приемщик в своем конструкторе, то scoped_allocator_adaptor<A>::construct()
выполняет либо:
std::allocator_traits<outer_allocator_type>::construct(outer_allocator(), void_ptr, obj, inner_allocator());
или
std::allocator_traits<outer_allocator_type>::construct(outer_allocator(), void_ptr, std::allocator_arg, inner_allocator(), obj);
то есть. адаптер передает дополнительные аргументы, когда он вызывает construct()
на своем вложенном распределителе, так что объект будет построен с помощью распределителя. inner_allocator_type
- это еще одна специализация scoped_allocator_adaptor
, поэтому, если тип элемента также является контейнером, он использует тот же протокол для построения своих элементов, и распределитель может передаваться каждому элементу, даже если у вас есть контейнеры контейнеров контейнеров и т.д.
Таким образом, целью адаптера является обернуть существующий распределитель и выполнить все метапрограммирование и манипулирование аргументами конструктора для распространения распределителей из контейнера в его дочерние элементы.