Может ли кто-нибудь объяснить правила времени жизни объектов и неинициализированной памяти в C++ (контекст: `std::inplace_vector`)?

Я пытался реализовать свою собственную версию std::inplace_vector на C++26; то есть массив с динамически изменяемым размером и фиксированной емкостью во время компиляции, который также позволяет хранить элементы, не создаваемые по умолчанию.

Вначале я мог придумать единственный способ хранения элементов, не являющихся конструируемыми по умолчанию, — это сохранить union из T array[N]; в качестве поля. Это связано с тем, что ранее я читал, что объединение одного элемента можно использовать для предотвращения немедленной инициализации этого элемента (источник), что необходимо здесь для предотвращения автоматического построения значений T по умолчанию. Затем я бы использовал Place-new/delete (или, на самом деле, std::allocator_traits<Allocator>), чтобы напрямую инициализировать и уничтожать элементы, когда это необходимо.

Однако я просто подумал о возможной проблеме. Кажется, я слышал, что в C++ нельзя назначать объект размером N байт любой неинициализированной последовательности N необработанных байтов, потому что «время жизни» объекта должно уже начаться. В этом случае, если бы я создал my_inplace_vector по умолчанию, не было бы мне запрещено фактически присваивать непосредственно любому из элементов (т. е. my_inplace_vector[1] = some initializer), потому что упаковка поля array в union предотвращает начало времени жизни его элементов ?

Я прочитал страницу cppreference о времени жизни, но не могу найти соответствующий раздел для объектов, содержащихся в массиве, содержащемся в объединении, поэтому я не уверен, сработает ли время жизни в этом случае. Может кто-нибудь объяснить мне правила?

Разве это не то же самое, что и llvm::SmallVector?

n. m. could be an AI 15.08.2024 17:56

AFAIK все еще невозможно реализовать вектор в рамках правил C++.

NathanOliver 15.08.2024 17:57

Вам необходимо начать время существования отдельных элементов массива с помощью нового размещения (при изменении размера вектора) и уничтожить их, когда вектор умирает (путем ручного вызова деструктора).

HolyBlackCat 15.08.2024 17:59

@NathanOliver Это std::launder не исправит?

HolyBlackCat 15.08.2024 17:59

" Реализация static_vector: насколько это может быть сложно? - Дэвид Стоун - CppCon 2021 " youtube.com/watch?v=I8QJLGI0GOE

alfC 15.08.2024 18:01

@HolyBlackCat Это могло бы исправить. Мне нужно посмотреть, смогу ли я найти сообщение, о котором думаю.

NathanOliver 15.08.2024 18:01

@HolyBlackCat Проблема заключается в арифметике указателей. Поскольку вектор будет содержать буфер, а не массив T, то для индексации одного элемента вам нужно будет рассматривать буфер как массив, и я не уверен, что отмывание позволяет вам это сделать, поскольку не существует фактического там массив T. Просто куча T в байтовом буфере

NathanOliver 15.08.2024 18:06

@NathanOliver: это исправляет неявное создание объектов. Даже если сам T не может пройти IOC, массив может. Таким образом, массив может проявляться в хранилище по мере необходимости, чтобы арифметика указателей работала.

Nicol Bolas 15.08.2024 18:08

@NicolBolas Приятно знать. Спасибо.

NathanOliver 15.08.2024 18:15

@NathanOliver Или вы можете использовать массив unsigned char.

HolyBlackCat 15.08.2024 19:39
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
10
122
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Предположим, у нас есть что-то вроде

union Storage {
    T arr[N];
    Storage() {}
};

и ваш inplace_vector содержит член Storage storage;, который изначально инициализируется по умолчанию.

Тогда действительно время жизни массива arr и его элементов еще не началось, и, как правило, это запрещает большинство видов использования элементов массива до тех пор, пока не начнется их время жизни.

Теперь, если T — тип с тривиальным присваиванием, то существует исключение, специфичное для объединений. В таком случае написание

storage.arr[i] = /*...*/;

где = использует тривиальный оператор присваивания или встроенный оператор присваивания, неявно приведет к тому, что время существования массива и i-го элемента массива начнется до того, как ему будет присвоено значение. Это конкретное правило действует только в определенных формах выражений, которые непосредственно называют объект объединения, за которым следует комбинация доступа к члену . и встроенной индексации массива в левой части =. Точные правила см. [class.union.general]/5.

Итак, если ваш тип T тривиален, то проблем нет. Вы действительно можете реализовать это, как вы предложили.


Однако если T не имеет тривиального присваивания в выражении, то

storage.arr[i] = /*...*/;

действительно будет иметь неопределенное поведение, поскольку время существования storage.arr[i] еще не началось, когда вы пытаетесь вызвать для него функцию-член operator=.

Вместо этого вам нужно явно начать жизнь объекта. Вы можете сделать это с помощью нового размещения или, что удобно, с помощью std::construct_at (который представляет собой оболочку вокруг конкретной формы нового размещения с дополнительным преимуществом, заключающимся в возможности использования во время компиляции).

Итак, вы бы написали, например.

std::construct_at(storage.arr[i]);

для создания i-го элемента перед его присвоением (или вы могли бы предоставить аргументы конструктора в качестве дополнительных аргументов для std::construct_at).

К сожалению, на самом деле это не работает, поскольку будет создан новый объект типа T и начнется время существования этого объекта. Вы хотите, чтобы этот новый объект заменил старый объект T по i-му индексу (время существования которого никогда не начиналось) и стал i-м элементом объекта массива.

Но требованием для того, чтобы вновь созданный объект стал элементом объекта массива, является то, что сам объект массива находится в пределах своего существования. См. [intro.object]/2.1.

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

Я думаю, что всегда безопасно начинать жизнь объекта массива с std::start_lifetime_as (предполагая, что T не является const?):

std::start_lifetime_as<T[N]>(&storage.arr);

Вы можете сделать это в конструкторе по умолчанию Storage, и его преимуществом является отсутствие фактического доступа к хранилищу.

Теперь вы можете использовать std::construct_at для индивидуального создания элементов массива, как предлагалось ранее.


Однако, поскольку вам в любом случае необходимо заново разместить каждый отдельный элемент, существует более простой подход: вы можете просто использовать массив, объявленный как

alignas(T) std::byte storage[N*sizeof(T)];

для хранения ваших объектов. Когда начинается время жизни массива std::byte, он неявно создает вложенные в него объекты типа неявного времени жизни и начинает их жизнь, в частности для самого объекта массива T[N], но не его элементов (если они также не имеют неявного времени жизни). типы). (Обратите внимание, что это относится только к массивам типа std::byte или unsigned char и, конечно, выравнивание и размер должны быть правильно указаны, как указано выше.)

Затем вы можете создавать объекты с помощью std::construct_at(reinterpret_cast<T*>(storage + i*sizeof(T))) и получать указатели на них с помощью std::launder(reinterpret_cast<T*>(storage + i*sizeof(T))).


В любом случае у обоих подходов есть проблема: они не работают с константными выражениями во время компиляции. std::start_lifetime_as не является constexpr, а reinterpret_cast/std::launder не допускается в константных выражениях.

Вероятно, именно поэтому std::inplace_vector было указано, что его можно использовать во время компиляции, только если тип элемента тривиален. См. окончательную редакцию предложения inplace_vector[P0843R14].

В предложении также упоминается, что для поддержки constexpr требуется инициализация значений, но я думаю, что это только потому, что они также хотят, чтобы std::inplace_vector было тривиально копируемым, если тип элемента можно тривиально копировать, и в этом случае инициализация по умолчанию может вызвать неопределенное поведение из-за копирование неопределенных значений.

В документе также есть ссылки на эталонную реализацию. Эта эталонная реализация использует подход с массивом std::byte для нетривиальных типов (в этом случае поддержка constexpr не требуется) и напрямую использует обычный (инициализируемый значением) массив для тривиальных типов. Он не использует союз.

В эталонной реализации отсутствует вызов std::launder, но это технически необходимо, чтобы избежать UB. Учитывая, что ни один компилятор, похоже, не выполняет никакой оптимизации, на которую мог бы повлиять этот вызов, вероятно, он намеренно пропустил ее. Также в настоящее время есть предложение изменить правила, чтобы std::launder в этом случае не требовалось, см. https://github.com/cplusplus/papers/issues/1703.

Другие вопросы по теме

Различные примечания для неинициализированного члена внутри общей лямбды в GCC 9 и GCC 10
Почему неявно сгенерированный конструктор отличается от конструктора, предоставленного пользователем?
Использование ранее установленных полей при инициализации структуры составными литералами
JavaFX не инициализирует представления
Есть ли выигрыш в производительности от использования алгоритмов c++ std(::ranges)::uninitialized_... и стоит ли не использовать constexpr?
Приводит ли наличие вектора, содержащего структуры с неинициализированными членами, к неопределенному поведению?
Попытка инициализировать объекты класса внутри структуры приводит к ошибкам сегментации
Существует ли альтернативный синтаксис для инициализации константного указателя на константные данные?
Почему Swift разрешает использование переменных перед объявлением в глобальной области, но не внутри функций?
Не удалось вычислить аргумент шаблона класса при использовании назначенных инициализаторов с инициализацией списка

Похожие вопросы

Как вложенные векторы выглядят в памяти?
Почему перегрузка оператора предварительного приращения не вызывается для моего пользовательского класса итератора в C++?
Синтаксический сахар для синхронизации блока кода в C++
Как ограничить функцию шаблона определенными типами?
Visual Studio C++: копирование проектов и решений с одного компьютера на другой (github не работает)
Странное поведение кнопки EnableWindow при нажатии
Можно ли с уверенностью предположить, что 32-битные числа с плавающей запятой можно напрямую сравнивать друг с другом, если значение соответствует мантиссе?
Практическое руководство: функция C++, которая настраивает тип возвращаемого значения в соответствии с потребностями вызывающей стороны
Возвращает ли low_bound() один и тот же результат с обратными итераторами вектора в порядке возрастания и прямыми итераторами вектора в порядке убывания?
Почему std::make_format_args ожидает неконстантную ссылку