Рассмотрите функцию и определите, допустим ли доступ к возвращённому контейнеру:
#include <cstddef>
#include <memory_resource>
#include <vector>
std::pmr::vector<int> make_vector() {
std::byte buffer[128];
std::pmr::monotonic_buffer_resource pool{buffer, sizeof buffer};
std::pmr::vector<int> values{&pool};
values.push_back(42);
return values;
}
int main() {
auto values = make_vector();
return values[0];
}
Объясните последствия для времени жизни памяти и владения ресурсом.
Доступ к values[0] недопустим: возвращённый контейнер использует память и memory_resource, уничтоженные при выходе из make_vector(). Контейнер не владеет переданным std::pmr::memory_resource и не продлевает время жизни буфера или самого ресурса. Результат — неопределённое поведение, включая неопределённое поведение уже при последующем уничтожении контейнера.
Полиморфные аллокаторы std::pmr появились как стандартный способ отделить тип контейнера от конкретной стратегии выделения памяти. Это позволяет выбирать ресурс во время выполнения: например, использовать стековый буфер, пул или обычное выделение через new.
Такое разделение даёт гибкость и может уменьшить количество выделений, но переносит ответственность за время жизни ресурса на разработчика. std::pmr управляет способом выделения, а не автоматически становится владельцем переданного ресурса.
values хранит std::pmr::polymorphic_allocator<int>, внутри которого находится указатель на pool. Сам pool хранит указатель на buffer и использует этот буфер для размещения элементов.
При выходе из make_vector() сначала уничтожается локальный values, затем pool, а затем становится недействительным локальный buffer. Однако при возврате контейнера его объект продолжает существовать в вызывающем коде, хотя связанные с ним ресурсы уже уничтожены.
Обращение к элементу читает память, время жизни которой завершилось. Кроме того, деструктор контейнера может попытаться вызвать deallocate через уже уничтоженный pool, что также является неопределённым поведением.
Владение здесь разделено между несколькими объектами:
buffer предоставляет исходную область памяти, но не принадлежит контейнеру;pool управляет выделениями из этой области и при необходимости ресурсом верхнего уровня;std::pmr::vector хранит только ссылку в виде указателя на memory_resource, но не владеет им;pool.Перемещение values при возврате не исправляет ситуацию. Аллокатор и указатель на ресурс перемещаются вместе с контейнером, но сам pool не перемещается и не превращается в часть владения контейнера.
Безопасные варианты зависят от задачи:
std::vector<int> или скопировать элементы в контейнер с ресурсом, живущим дольше. Это безопасно, но может потребовать копирования и динамических выделений.Например, безопасная структура может выглядеть так:
В Data контейнер уничтожится раньше pool, а pool — раньше buffer. Но такой объект нельзя бездумно копировать или перемещать: внутренние указатели ресурсов могут ссылаться на исходный объект. Для него обычно явно задают допустимую семантику владения и перемещения.
Сервис формирует временные сообщения и хочет избежать множества мелких выделений. Разработчик создаёт monotonic_buffer_resource с локальным буфером внутри функции и возвращает std::pmr::vector наружу.
Рассматривались варианты:
std::pmr::vector с локальным ресурсом — быстро, но приводит к неопределённому поведению;std::vector перед возвратом — безопасно, но добавляет копирование;Выбран последний вариант для внутреннего API: ресурс и контейнер принадлежат одной обёртке, а порядок полей фиксирует корректное уничтожение. Для публичного API, где такая семантика была бы слишком сложной, выбрали обычный std::vector.
std::pmr::vector отдельные блоки памяти при уничтожении?Сам контейнер вызывает deallocate у своего ресурса, но конкретное поведение определяется ресурсом. У std::pmr::monotonic_buffer_resource отдельное освобождение обычно не возвращает память по одному блоку: ресурс освобождает накопленную память при своём уничтожении или вызове release(). Это не меняет требования: сам ресурс должен оставаться живым до завершения работы контейнера.
memory_resource, если исходный буфер уже уничтожен?Нет. Ресурс может ссылаться на внешний буфер, например массив на стеке, и не обязан владеть им. Даже живой pool не делает уничтоженный buffer действительным. Все внешние области памяти, используемые ресурсом, должны жить не меньше объектов, размещённых в них.
std::pmr::vector в другой контейнер?Не обязательно. Обычное перемещение сохраняет связь с исходным memory_resource, поэтому новый контейнер может по-прежнему ссылаться на уже уничтоженный ресурс. Чтобы изменить владельца памяти, нужно явно создать контейнер с новым, достаточно долгоживущим ресурсом и перенести или скопировать в него элементы.