Я пытался реализовать свою собственную версию 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 о времени жизни, но не могу найти соответствующий раздел для объектов, содержащихся в массиве, содержащемся в объединении, поэтому я не уверен, сработает ли время жизни в этом случае. Может кто-нибудь объяснить мне правила?
AFAIK все еще невозможно реализовать вектор в рамках правил C++.
Вам необходимо начать время существования отдельных элементов массива с помощью нового размещения (при изменении размера вектора) и уничтожить их, когда вектор умирает (путем ручного вызова деструктора).
@NathanOliver Это std::launder
не исправит?
" Реализация static_vector: насколько это может быть сложно? - Дэвид Стоун - CppCon 2021 " youtube.com/watch?v=I8QJLGI0GOE
@HolyBlackCat Это могло бы исправить. Мне нужно посмотреть, смогу ли я найти сообщение, о котором думаю.
@HolyBlackCat Проблема заключается в арифметике указателей. Поскольку вектор будет содержать буфер, а не массив T
, то для индексации одного элемента вам нужно будет рассматривать буфер как массив, и я не уверен, что отмывание позволяет вам это сделать, поскольку не существует фактического там массив T
. Просто куча T
в байтовом буфере
@NathanOliver: это исправляет неявное создание объектов. Даже если сам T
не может пройти IOC, массив может. Таким образом, массив может проявляться в хранилище по мере необходимости, чтобы арифметика указателей работала.
@NicolBolas Приятно знать. Спасибо.
@NathanOliver Или вы можете использовать массив unsigned char
.
Предположим, у нас есть что-то вроде
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.
Разве это не то же самое, что и llvm::SmallVector?