Я пытался реализовать свою собственную версию 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?