понедельник, 9 марта 2026 г.

[prog.c++] Как будто бы недоделки в системе C++ных аллокаторов

Потратил некоторое время на адаптацию хитрого контейнера под аллокаторы. В процессе адаптации столкнулся с несколькими вещами, которые выглядят как недоделки. Как будто бы до них просто руки не дошли. Ну или я просто чего-то полезного не нашел.


Первая штука -- это отсутствие каких-то простых инструментов для создания нового объекта посредством аллокатора.

Если нам не нужно связываться с аллокаторами, то мы просто вызываем new T (или make_unique<T> или make_shared<T>) и все.

А вот в случае с аллокаторами сперва нужно вызвать у аллокатора allocate чтобы получить блок памяти под объект. А затем нужно вызывать у аллокатора construct, чтобы сконструировать объект в этом блоке. Т.е. два действия вместо одного. При этом, если construct бросает исключение, то нужно вручную освободить блок памяти посредством обращения к deallocate у аллокатора.

Но вот простого метода, который бы сперва сам вызвал allocate и следом construct, в стандартной библиотеке я не нашел. Есть там make_obj_using_allocator, но он вроде как для совсем другого.

Так же нет простого метода, который бы взял ссылку на аллокатор и указатель на удаляемый объект и сам бы сперва вызвал destroy для объекта, а потом deallocate для блока памяти. А хотелось бы иметь готовый, а не делать самостоятельно на коленке.

И да, я знаю, что allocate/construct и destroy/deallocate вызываются через allocator_traits, просто не упоминаю об этом для простоты изложения.


В C++17 в стандартную библиотеку добавили std::pmr::memory_resource. И, вроде как, имеет смысл делать свои арены в виде наследников от memory_resource.

Но, как я понял, дизайн memory_resource направлен на то, чтобы информировать о возникающих ошибках только через выброс исключения. Поскольку метод do_allocate должен бросать исключение при невозможности выделить память.

И тут вопрос: а как быть, если мы не хотим получать исключение? Например, нам хотелось бы иметь аналог new(std::nothrow). Типа попробовали выделить память, если не получилось, то у нас на руках нулевой указатель и мы можем попробовать обработать эту ситуацию без try..catch блока (ведь try..catch -- это дорого).

Мне кажется, что в memory_resource напрашивается метод allocate формата:

void *
allocate(std::nothrow_t,
  std::size_t bytes,
  std::size_t alignment = alignof(std::max_align_t));

и соответствующий ему метод do_allocate.

Но почему-то этого нет 🙁


У аллокатора есть свойство, которые выражаются вложенным типом propagate_on_container_copy_assignment. Если оно эквивалентно std::true_type, то аллокатор должен копироваться при копировании содержимого контейнера в операторе копирования.

При этом у аллокатора есть метод select_on_container_copy_construction, который должен вызываться у аллокатора в конструкторе копирования контейнера. Т.е. вот в этом случае:

some_container original{ ... };
...
some_container copy{ original }; // (1)

В точке (1) должен быть вызов original.get_allocator().select_on_container_copy_construction().

Что мне кажется странным и несколько недодуманным, так это то, что потенциально propagate_on_container_copy_assignment и select_on_container_copy_construction могут быть не согласованы.

Т.е. свойство propagate_on_container_copy_assignment может быть std::false_type (а по умолчанию это так и есть), при этом select_on_container_copy_construction может возвращать тот же самый аллокатор. Что приведет к тому, что в конструкторе копирования у нас будет копироваться аллокатор из контейнера-источника. А вот в операторе копирования мы аллокатор из источника копировать уже не будем.

А может быть и другой вариант: propagate_on_container_copy_assignment -- это std::true_type, тогда как select_on_container_copy_construction будет возвращать новый экземпляр аллокатора (не равный исходному). Тогда в конструкторе копирования мы будем получать новый экземпляр аллокатора, а в операторе копирования -- будем получать копию аллокатора из контейнера-источника.

И за отсутствием таких рассогласований должен следить сам программист. Не то, чтобы это было сложно, но там где есть вероятность что-то забыть, рано или поздно вероятность воплотится в действительность.

Конкретно эту штуку, наверное, нельзя назвать недоделкой. Но не покидает впечатление, что есть во всей этой кухне некоторая недосказанность, разбираться которой приходится каждому, кто погружается в тему аллокаторов. Не менее вероятно, что я просто не понимаю всей глубины замыслов авторов этой части стандартной библиотеки.