Другое

Неявные типы времени жизни в std::allocator: Полное руководство

Узнайте, как std::allocator обрабатывает неявные типы времени жизни и когда объекты создаются в выделенном хранилище. Изучите лучшие практики безопасного доступа до явного конструирования.

Доступ к объектам типов с неявным временем жизни в хранилище, предоставленном std::allocator, до явного конструирования

В C++, std::allocator::allocate(std::size_t n) выделяет хранилище для массива из n объектов типа T, но не начинает время жизни фактических объектов типа T. Для типов без неявного времени жизни обращение к этим объектам до конструирования является неопределенным поведением, поскольку объекты еще не существуют.

Однако для объектов типов с неявным временем жизни (таких как скалярные типы), стандарт утверждает, что хранилище, созданное с помощью ::operator new, неявно создает объекты. Это raises вопрос:

  1. При использовании std::allocator для выделения хранилища для массива типов с неявным временем жизни (например, std::array<int, N>), сам массив и его под-объекты начинают свое время жизни неявным образом?

  2. Более общим образом, под-объекты типов с неявным временем жизни также создаются неявным образом, когда содержащий их объект создается неявным образом?

Рассмотрим этот пример:

cpp
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> ситуация имеет нюансы.

Содержание

Понимание неявного создания объектов

Неявное создание объектов (IOC) происходит, когда определенные операции создают объекты без явных вызовов конструкторов. Согласно стандарту C++, операции такие как ::operator new неявно создают объекты типов с неявным временем жизни в выделенном хранилище.

Типы с неявным временем жизни включают:

  • Скалярные типы (такие как int, float, char)
  • Тривиально копируемые типы
  • Массивы типов с неявным временем жизни

Однако, как отмечено в требованиях C++ с именем ImplicitLifetimeType, «Такие операции не запускают время жизни под-объектов таких объектов, которые сами не являются типами с неявным временем жизни».

Поведение std::allocator::allocate

Метод std::allocator::allocate имеет специфическое поведение относительно времени жизни:

«Эта функция создает массив типа T[n] в хранилище и запускает его время жизни, но не запускает время жизни ни одного из его элементов.»

Это ключевое различие: время жизни объекта массива начинается, но время жизни отдельных элементов не начинается автоматически, если только T не является типом с неявным временем жизни.

Время жизни массива vs элементов

Ключевой вопрос заключается в том, являются ли под-объекты типов с неявным временем жизни также неявно создаваемыми, когда содержащий объект создается неявно.

Из результатов исследования ответ имеет нюансы:

  1. Время жизни массива: Когда вы вызываете allocator.allocate(1) для std::array<int, N>, сам объект массива начинает свое время жизни.

  2. Время жизни элементов: Отдельные элементы int внутри массива не имеют автоматически запущенного времени жизни, даже несмотря на то, что int является типом с неявным временем жизни.

Это связано с тем, что стандарт явно гласит, что операции такие как `std::allocator::allocate» «не запускают время жизни ни одного из его элементов» независимо от типа элемента.

Рассмотрения времени жизни std::array

В вашем конкретном примере с std::array<int, N> ситуация более сложная, чем с простыми массивами:

cpp
auto ptr = allocator.allocate(1);  // Создает массив std::array<int, N>[1]
Type_t* it = ptr->data();           // Получает доступ к внутренним элементам массива

Сам объект std::array имеет запущенное время жизни, но его внутренние элементы массива - нет. Согласно обсуждению на Stack Overflow, доступ к этим элементам до явного конструирования является неопределенным поведением.

Это подтверждается тем фактом, что хотя int является типом с неявным временем жизни, обертка std::array<int, N> таковой не является, и время жизни элементов должно быть запущено явно.

Практические последствия

Для вашего примера кода:

cpp
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. Попытка чтения из или записи в эти элементы до конструирования будет неопределенным поведением.

Лучшие практики

  1. Всегда явно конструируйте элементы при использовании std::allocator::allocate, даже для типов с неявным временем жизни внутри контейнеров таких как std::array.

  2. Используйте placement new или std::construct_at для явного конструирования:

    cpp
    std::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));
    }
    
  3. Рассмотрите возможность использования контейнеров, которые автоматически обрабатывают конструирование, таких как std::vector, который правильно управляет временем жизни элементов.

  4. Будьте осторожны с переинтерпретацией - даже для типов с неявным временем жизни, доступ к памяти через разные типы до правильной инициализации может привести к неопределенному поведению.

Заключение

В заключение, при использовании std::allocator для выделения хранилища для std::array<int, N>:

  1. Сам объект std::array имеет запущенное время жизни благодаря allocate()
  2. Внутренние элементы массива int НЕ имеют автоматически запущенного времени жизни
  3. Доступ к этим элементам до явного конструирования является неопределенным поведением
  4. Вы должны явно конструировать как объект std::array, так и его элементы
  5. Это поведение отличается от сырых массивов, выделенных с помощью ::operator new, которые могут иметь разные правила неявного времени жизни

Ключевой вывод заключается в том, что std::allocator::allocate запускает время жизни объекта массива, но не его элементов, независимо от того, является ли тип элемента типом с неявным временем жизни.

Источники

  1. Lifetime - cppreference.com
  2. std::allocator::allocate - cppreference.com
  3. C++ named requirements: ImplicitLifetimeType
  4. How to create an array and start its lifetime without starting the lifetime of any of its elements? - Stack Overflow
  5. Implicit object creation, arrays, and non-implicit lifetime objects - std-discussion
Авторы
Проверено модерацией
Модерация