Неявные типы времени жизни в std::allocator: Полное руководство
Узнайте, как std::allocator обрабатывает неявные типы времени жизни и когда объекты создаются в выделенном хранилище. Изучите лучшие практики безопасного доступа до явного конструирования.
Доступ к объектам типов с неявным временем жизни в хранилище, предоставленном std::allocator, до явного конструирования
В C++, std::allocator
Однако для объектов типов с неявным временем жизни (таких как скалярные типы), стандарт утверждает, что хранилище, созданное с помощью ::operator new, неявно создает объекты. Это raises вопрос:
-
При использовании std::allocator для выделения хранилища для массива типов с неявным временем жизни (например, std::array<int, N>), сам массив и его под-объекты начинают свое время жизни неявным образом?
-
Более общим образом, под-объекты типов с неявным временем жизни также создаются неявным образом, когда содержащий их объект создается неявным образом?
Рассмотрим этот пример:
static constexpr std::size_t N = 100;
using Type_t = int;
void process() {
auto allocator = std::allocator<std::array<Type_t, N>>{};
auto ptr = allocator.allocate(1);
// ptr указывает на std::array<Type_t, N>[1]
Type_t* it = ptr->data();
// Обращение к данным std::array<Type_t, N>:
// Находится ли он в своем времени жизни?
// Начало времени жизни элементов массива
for (std::size_t i = 0; i != N; ++i) {
std::construct_at(it, static_cast<Type_t>(i * i));
++it;
}
allocator.deallocate(ptr, 1);
}
Началось ли неявное время жизни у std::array, выделенного с помощью allocate(1), и можно ли безопасно обращаться к его элементам до явного конструирования?
При использовании std::allocator для выделения хранилища для массива типов с неявным временем жизни, сам объект массива начинает свое время жизни, но отдельные элементы не имеют своего времени жизни, если только они сами не являются типами с неявным временем жизни. В вашем примере с std::array<int, N> ситуация имеет нюансы.
Содержание
- Понимание неявного создания объектов
- Поведение std::allocator::allocate
- Время жизни массива vs элементов
- Рассмотрения времени жизни std::array
- Практические последствия
- Лучшие практики
Понимание неявного создания объектов
Неявное создание объектов (IOC) происходит, когда определенные операции создают объекты без явных вызовов конструкторов. Согласно стандарту C++, операции такие как ::operator new неявно создают объекты типов с неявным временем жизни в выделенном хранилище.
Типы с неявным временем жизни включают:
- Скалярные типы (такие как
int,float,char) - Тривиально копируемые типы
- Массивы типов с неявным временем жизни
Однако, как отмечено в требованиях C++ с именем ImplicitLifetimeType, «Такие операции не запускают время жизни под-объектов таких объектов, которые сами не являются типами с неявным временем жизни».
Поведение std::allocator::allocate
Метод std::allocator::allocate имеет специфическое поведение относительно времени жизни:
«Эта функция создает массив типа T[n] в хранилище и запускает его время жизни, но не запускает время жизни ни одного из его элементов.»
Это ключевое различие: время жизни объекта массива начинается, но время жизни отдельных элементов не начинается автоматически, если только T не является типом с неявным временем жизни.
Время жизни массива vs элементов
Ключевой вопрос заключается в том, являются ли под-объекты типов с неявным временем жизни также неявно создаваемыми, когда содержащий объект создается неявно.
Из результатов исследования ответ имеет нюансы:
-
Время жизни массива: Когда вы вызываете
allocator.allocate(1)дляstd::array<int, N>, сам объект массива начинает свое время жизни. -
Время жизни элементов: Отдельные элементы
intвнутри массива не имеют автоматически запущенного времени жизни, даже несмотря на то, чтоintявляется типом с неявным временем жизни.
Это связано с тем, что стандарт явно гласит, что операции такие как `std::allocator::allocate» «не запускают время жизни ни одного из его элементов» независимо от типа элемента.
Рассмотрения времени жизни std::array
В вашем конкретном примере с std::array<int, N> ситуация более сложная, чем с простыми массивами:
auto ptr = allocator.allocate(1); // Создает массив std::array<int, N>[1]
Type_t* it = ptr->data(); // Получает доступ к внутренним элементам массива
Сам объект std::array имеет запущенное время жизни, но его внутренние элементы массива - нет. Согласно обсуждению на Stack Overflow, доступ к этим элементам до явного конструирования является неопределенным поведением.
Это подтверждается тем фактом, что хотя int является типом с неявным временем жизни, обертка std::array<int, N> таковой не является, и время жизни элементов должно быть запущено явно.
Практические последствия
Для вашего примера кода:
Type_t* it = ptr->data();
// Находится ли он в своем времени жизни? Нет, еще нет
for (std::size_t i = 0; i != N; ++i) {
std::construct_at(it, static_cast<Type_t>(i * i)); // Запускает время жизни элемента
++it;
}
Элементы, доступ к которым получен через ptr->data(), не находятся в своем времени жизни, пока вы не сконструируете их явно с помощью std::construct_at. Попытка чтения из или записи в эти элементы до конструирования будет неопределенным поведением.
Лучшие практики
-
Всегда явно конструируйте элементы при использовании
std::allocator::allocate, даже для типов с неявным временем жизни внутри контейнеров таких какstd::array. -
Используйте placement new или
std::construct_atдля явного конструирования:cppstd::construct_at(ptr); // Конструирует объект std::array for (std::size_t i = 0; i != N; ++i) { std::construct_at(&(*ptr)[i], static_cast<Type_t>(i * i)); } -
Рассмотрите возможность использования контейнеров, которые автоматически обрабатывают конструирование, таких как
std::vector, который правильно управляет временем жизни элементов. -
Будьте осторожны с переинтерпретацией - даже для типов с неявным временем жизни, доступ к памяти через разные типы до правильной инициализации может привести к неопределенному поведению.
Заключение
В заключение, при использовании std::allocator для выделения хранилища для std::array<int, N>:
- Сам объект
std::arrayимеет запущенное время жизни благодаряallocate() - Внутренние элементы массива
intНЕ имеют автоматически запущенного времени жизни - Доступ к этим элементам до явного конструирования является неопределенным поведением
- Вы должны явно конструировать как объект
std::array, так и его элементы - Это поведение отличается от сырых массивов, выделенных с помощью
::operator new, которые могут иметь разные правила неявного времени жизни
Ключевой вывод заключается в том, что std::allocator::allocate запускает время жизни объекта массива, но не его элементов, независимо от того, является ли тип элемента типом с неявным временем жизни.
Источники
- Lifetime - cppreference.com
- std::allocator::allocate - cppreference.com
- C++ named requirements: ImplicitLifetimeType
- How to create an array and start its lifetime without starting the lifetime of any of its elements? - Stack Overflow
- Implicit object creation, arrays, and non-implicit lifetime objects - std-discussion