Программирование C++Управление памятьюРазработчик C++ системного программного обеспечения

Рассмотрите функцию и определите, допустим ли доступ к возвращённому контейнеру: пример с кодом Объясните п...

Рассмотрите функцию и определите, допустим ли доступ к возвращённому контейнеру:

#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];
}

Объясните последствия для времени жизни памяти и владения ресурсом.

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Доступ к 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 не перемещается и не превращается в часть владения контейнера.

Безопасные варианты зависят от задачи:

  1. Вернуть обычный std::vector<int> или скопировать элементы в контейнер с ресурсом, живущим дольше. Это безопасно, но может потребовать копирования и динамических выделений.
  2. Передать ресурс в функцию извне и гарантировать, что вызывающий код уничтожит контейнер раньше ресурса. Это эффективно, но требует явного контракта времени жизни.
  3. Объединить буфер, ресурс и контейнер в одном владельце. Поля должны объявляться в порядке: сначала буфер, затем ресурс, затем контейнер. Поля уничтожаются в обратном порядке.

Например, безопасная структура может выглядеть так:

#include <cstddef> #include <memory_resource> #include <vector> struct Data { alignas(std::max_align_t) std::byte buffer[128]; std::pmr::monotonic_buffer_resource pool{buffer, sizeof buffer}; std::pmr::vector<int> values{&pool}; };

В Data контейнер уничтожится раньше pool, а pool — раньше buffer. Но такой объект нельзя бездумно копировать или перемещать: внутренние указатели ресурсов могут ссылаться на исходный объект. Для него обычно явно задают допустимую семантику владения и перемещения.

Ситуация из практики

Сервис формирует временные сообщения и хочет избежать множества мелких выделений. Разработчик создаёт monotonic_buffer_resource с локальным буфером внутри функции и возвращает std::pmr::vector наружу.

Рассматривались варианты:

  • вернуть std::pmr::vector с локальным ресурсом — быстро, но приводит к неопределённому поведению;
  • копировать данные в обычный std::vector перед возвратом — безопасно, но добавляет копирование;
  • передавать ресурс от вызывающего кода — эффективно, но усложняет интерфейс и контракт времени жизни;
  • вернуть объект-обёртку, содержащий буфер, ресурс и контейнер — сохраняет локальность выделений и корректно связывает времена жизни.

Выбран последний вариант для внутреннего API: ресурс и контейнер принадлежат одной обёртке, а порядок полей фиксирует корректное уничтожение. Для публичного API, где такая семантика была бы слишком сложной, выбрали обычный std::vector.

Что кандидаты часто упускают

  1. Освобождает ли std::pmr::vector отдельные блоки памяти при уничтожении?

Сам контейнер вызывает deallocate у своего ресурса, но конкретное поведение определяется ресурсом. У std::pmr::monotonic_buffer_resource отдельное освобождение обычно не возвращает память по одному блоку: ресурс освобождает накопленную память при своём уничтожении или вызове release(). Это не меняет требования: сам ресурс должен оставаться живым до завершения работы контейнера.

  1. Достаточно ли продлить жизнь memory_resource, если исходный буфер уже уничтожен?

Нет. Ресурс может ссылаться на внешний буфер, например массив на стеке, и не обязан владеть им. Даже живой pool не делает уничтоженный buffer действительным. Все внешние области памяти, используемые ресурсом, должны жить не меньше объектов, размещённых в них.

  1. Исправит ли проблему перемещение std::pmr::vector в другой контейнер?

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