В стандарте С++ 11 мы имеем std::scoped_allocator_adapter
в библиотеке управления динамической памятью. Каковы наиболее важные варианты использования этого класса?
Какова цель std:: scoped_allocator_adapter?
Ответ 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
, поэтому, если тип элемента также является контейнером, он использует тот же протокол для построения своих элементов, и распределитель может передаваться каждому элементу, даже если у вас есть контейнеры контейнеров контейнеров и т.д.
Таким образом, целью адаптера является обернуть существующий распределитель и выполнить все метапрограммирование и манипулирование аргументами конструктора для распространения распределителей из контейнера в его дочерние элементы.
Ответ 2
Предположим, что у вас есть генератор арендной платы с выражением состояния Alloc
с конструктором Alloc(Arena&)
, который позволяет получить некоторую специальную производительность для вашего приложения и сказать, что вы используете вложенную иерархию контейнеров, например:
using InnerCont = std::vector<int, Alloc<int>>;
using OuterCont = std::vector<InnerCont, std::scoped_allocator_adaptor<Alloc<InnerCont>>>;
Здесь использование scoped_allocator_adaptor
позволит вам распространять объект арены, используемый для инициализации вашего распределителя от внешнего к внутреннему контейнеру следующим образом:
auto my_cont = OuterCont{std::scoped_allocator_adaptor(Alloc<InnerCont>{my_arena})};
Достичь большей локальности данных и позволяет предварительно выделить одну большую арену памяти my_arena
для всей вашей иерархии контейнеров, а не только сделать my_arena
доступным для внешнего контейнера и потребовать петля над всеми контейнерами-контейнерами с другой ареной для каждого элемента на этом уровне.
Шаблон класса на самом деле представляет собой вариационный шаблон, который дает вам мелкомасштабный контроль над тем, какой тип распределителя использовать в каждом типе иерархии контейнеров. Предположительно, это дает более сложную структуру данных. Я должен признаться, что я не представляю разные распределители на разных уровнях в любом месте, но, возможно, крупные центры обработки данных с многомиллионными пользователями здесь имеют смысл).