Placement new для инициализации примитивного типа в самодельном union?
От: so5team https://stiffstream.com
Дата: 02.10.25 08:59
Оценка: 2 (1)
Доброго времени суток!

Нужна консультация от людей, которые знают формальную часть C++ лучше меня.

Делаю самодельный аналог union/variant. Что-то типа:

using void_ptr_t = void *;

template< typename T >
struct my_union_t
{
    alignas( std::max( alignof(T), alignof(void_ptr_t) ) )
    std::array< std::byte, std::max( sizeof(T), sizeof(void_ptr_t) ) > _content;

};

Т.е. это такое специализированное хранилище, в котором может быть либо экземпляр типа T, либо же указатель.
Либо вообще может ничего не быть, но это к делу не относится.

Важно то, что в качестве T может быть нетривиальный тип со своими конструкторами и деструктором. Вроде std::string или std::vector.

Когда мне нужно поместить в my_union_t::_content значение типа T, то естественным образом используется placement new:
template< typename T >
void
store_to( my_union_t<T> & dest, T value )
{
  new( dest._content.data() ) T{ std::move(value) };
}


Но вот нужен ли с формальной точки зрения placement new когда мне требуется туда сохранить значение указателя?
Т.е. должен ли я писать так:
template< typename T >
void
store_to( my_union_t<T> & dest, void_ptr_t value )
{
  new( dest._content.data() ) void_ptr_t{ value };
}

или же вполне достаточно и reinterpret_cast:
template< typename T >
void
store_to( my_union_t<T> & dest, void_ptr_t value )
{
  *(reinterpret_cast<void_ptr_t **>(dest._content.data())) = value;
}

?

Оно как бы понятно, что современные компиляторы без проблем возьмут и версию с reinterpret_cast. Но хочется понять, насколько это легально.

Сам думаю, что легальной является только версия с placement new и для T, и для void_ptr_t. Однако, не мешало бы получить какие-то подтверждения от знающих людей, чтобы быть уверенным.

PS. Вопрос о том, почему бы не взять std::variant выходит за рамки обсуждения, есть на то причины. Если брать обычный унаследованный из Си union, то для случаев, когда T является нетривиальным типом (вроде std::string) там свои заморочки, которые не делают код более понятным, скорее даже наоборот.
PPS. Про то, что для my_union_t нужны функции очистки содержимого и соответствующие процедуры для копирования/перемещения/swap я в курсе.
Re: Placement new для инициализации примитивного типа в само
От: sergii.p  
Дата: 02.10.25 09:52
Оценка:
Здравствуйте, so5team, Вы писали:

в функции store_to не должно быть проблем. Могут быть проблемы с функцией read

typename<T>
T& read(my_union_t<T>& src) {
  return *(reinterpret_cast<T*>(src._content.data()));
}

struct Data { int i; };
Data data1{1};
my_union_t<Data> storage;
store_to(storage, data1);
std::cout << read(storage).i;
Data data2{2};
store_to(storage, data2);
std::cout << read(storage).i; // тут компилятор может закешировать и вернуть 1


Чтобы избежать этого нужен std::launder. Но если его вызывать при каждом чтении, то компилятор уже ничего оптимизировать не будет, что тоже не очень хорошо.
Отредактировано 02.10.2025 10:20 sergii.p . Предыдущая версия .
Re[2]: Placement new для инициализации примитивного типа в самодельном union?
От: so5team https://stiffstream.com
Дата: 02.10.25 10:08
Оценка:
Здравствуйте, sergii.p, Вы писали:

SP>Чтобы избежать этого нужен std::launder.


Это понятно, std::launder используется.

SP>Но если его вызывать при каждом чтении, то компилытор уже ничего оптимизировать не будет, что тоже не очень хорошо.


Придется мириться, т.к. нет возможности сохранять где-либо возвращенный placement new указатель или даже хранить где-то признак того, что значение создано и может использоваться напрямую.
Re: Placement new для инициализации примитивного типа в самодельном union?
От: Zhendos  
Дата: 02.10.25 17:43
Оценка:
Здравствуйте, so5team, Вы писали:


S>Делаю самодельный аналог union/variant. Что-то типа:



S>Но вот нужен ли с формальной точки зрения placement new когда мне требуется туда сохранить значение указателя?


По идее для этого как раз придумали std::bit_cast
Re[2]: Placement new для инициализации примитивного типа в самодельном union?
От: so5team https://stiffstream.com
Дата: 03.10.25 04:12
Оценка: +2
Здравствуйте, Zhendos, Вы писали:

S>>Но вот нужен ли с формальной точки зрения placement new когда мне требуется туда сохранить значение указателя?


Z>По идее для этого как раз придумали std::bit_cast


Может я чего-то не понимаю, конечно, но мне кажется, что это другое.

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

В моем же случае внутри последовательности байт нужно разместить новый объект примитивного типа и корректно начать его lifetime с точки зрения языка.
Re[3]: Placement new для инициализации примитивного типа в самодельном union?
От: andrey.desman  
Дата: 03.10.25 05:22
Оценка: 6 (1) +1
Здравствуйте, so5team, Вы писали:

S>В моем же случае внутри последовательности байт нужно разместить новый объект примитивного типа и корректно начать его lifetime с точки зрения языка.


Да, для этого в 23 плюсах ввели std::start_lifetime_as. Если 23 нет, placement new будет безопасным вариантом (вместе с ручным вызовом деструктора при пересоздании).
Re[4]: Placement new для инициализации примитивного типа в самодельном union?
От: so5team https://stiffstream.com
Дата: 03.10.25 06:18
Оценка:
Здравствуйте, andrey.desman, Вы писали:

S>>В моем же случае внутри последовательности байт нужно разместить новый объект примитивного типа и корректно начать его lifetime с точки зрения языка.


AD>Да, для этого в 23 плюсах ввели std::start_lifetime_as. Если 23 нет, placement new будет безопасным вариантом (вместе с ручным вызовом деструктора при пересоздании).


В теории C++23 есть, но т.к. код должен компилироваться и на Linux, и на Windows, а Linux-ы могут быть нескольких вариантов и где-то std::start_lifetime_as может и не оказаться.

Однако, как я понимаю, тогда работа будет выглядеть как-то так:
// Сохранение значения.
template< typename T >
void
store_to( my_union_t<T> & dest, void_ptr_t value )
{
  std::memcpy( dest._content.data(), &value, sizeof(value) );
}

// Извлечение значения в виде указателя.
[[nodiscard]]
template< typename T >
void_ptr_t
read_pointer( const my_union_t<T> & src )
{
  return *(std::start_lifetime_as<const void_ptr_t *>(src._content.data()));
    // Тут еще поди догадайся чей lifetime начинается.
}


Есть ощущение, что вариант с placement new окажется и лаконичнее, и понятнее.
Re: Placement new для инициализации примитивного типа в самодельном union?
От: rg45 СССР  
Дата: 03.10.25 11:31
Оценка:
Здравствуйте, so5team, Вы писали:

S>Но вот нужен ли с формальной точки зрения placement new когда мне требуется туда сохранить значение указателя?

S>
S>template< typename T >
S>void
S>store_to( my_union_t<T> & dest, void_ptr_t value )
S>{
S>  *(reinterpret_cast<void_ptr_t **>(dest._content.data())) = value;
S>}
S>

S>?

S>Оно как бы понятно, что современные компиляторы без проблем возьмут и версию с reinterpret_cast. Но хочется понять, насколько это легально.


Если я правильно понимаю, то согласно 7.2.1/11 это UB.

S>Сам думаю, что легальной является только версия с placement new и для T, и для void_ptr_t. Однако, не мешало бы получить какие-то подтверждения от знающих людей, чтобы быть уверенным.


Я думаю, что здесь можно было бы заюзать std::start_lifetime_as:

template< typename T >
void
store_to( my_union_t<T> & dest, void_ptr_t value )
{
  *std::start_lifetime_as<void_ptr_t>(dest._content.data()) = value;
}
--
Справедливость выше закона. А человечность выше справедливости.
Re[2]: Placement new для инициализации примитивного типа в самодельном union?
От: sergii.p  
Дата: 03.10.25 12:26
Оценка: 8 (1)
Здравствуйте, rg45, Вы писали:

R>Если я правильно понимаю, то согласно 7.2.1/11 это UB.


так попадает же в исключения

if T_ref is similar ([conv.qual]) to:
— a char, unsigned char, or std​::​byte type.


std::array< std::byte, std::max( sizeof(T), sizeof(void_ptr_t) ) > _content;
Re[5]: Placement new для инициализации примитивного типа в с
От: andrey.desman  
Дата: 03.10.25 12:38
Оценка: 6 (1)
Здравствуйте, so5team, Вы писали:

S>Однако, как я понимаю, тогда работа будет выглядеть как-то так:


Создать объект надо один раз. В сторе должно быть создание (через new или sla), а в read просто reinterpret_cast.
Но раз в сторе всё равно запись делается, то толку от sla и нет.
В общем, как у тебя было изначально, так и хорошо.

S>Есть ощущение, что вариант с placement new окажется и лаконичнее, и понятнее.


Да.
Отредактировано 03.10.2025 12:39 andrey.desman . Предыдущая версия .
Re[3]: Placement new для инициализации примитивного типа в самодельном union?
От: andrey.desman  
Дата: 03.10.25 12:41
Оценка: 14 (2)
Здравствуйте, sergii.p, Вы писали:

R>>Если я правильно понимаю, то согласно 7.2.1/11 это UB.

SP>так попадает же в исключения

SP>

SP>if T_ref is similar ([conv.qual]) to:
SP>- a char, unsigned char, or std​::​byte type.


T_ref не существует. Его сначала надо создать. Через каст не создашь.
Re[4]: Placement new для инициализации примитивного типа в с
От: rg45 СССР  
Дата: 03.10.25 12:54
Оценка:
Здравствуйте, andrey.desman, Вы писали:

AD>Здравствуйте, sergii.p, Вы писали:


R>>>Если я правильно понимаю, то согласно 7.2.1/11 это UB.

SP>>так попадает же в исключения

SP>>

SP>>if T_ref is similar ([conv.qual]) to:
SP>>- a char, unsigned char, or std​::​byte type.


AD>T_ref не существует. Его сначала надо создать. Через каст не создашь.



Кстати да. В нашем случае std::byte — это Tobj. А Tref — это void*&, и он ни разу не similar to std::byte.
--
Справедливость выше закона. А человечность выше справедливости.
Отредактировано 04.10.2025 10:19 rg45 . Предыдущая версия . Еще …
Отредактировано 03.10.2025 12:57 rg45 . Предыдущая версия .
Re: Placement new для инициализации примитивного типа в самодельном union?
От: _NN_  
Дата: 06.10.25 16:02
Оценка:
Это как std::optional только без маркировки если есть значение или нет?
Типа https://github.com/akrzemi1/markable ?

Тут уже подсказали что и как start lifetime as конечно лучше всего, а если его нет то placement new.
Кстати memcpy не всегда можно использовать если у нас тип не тривиально копируемый.
А placement new не сработает если нет конструктора копирования.
В общем, для общего случая там работы хватить

Может стоит вместо копирования сделать как inplace/ make_optional ?
И передавать параметры как есть дальше, а там уже сам тип лучше тебя знает, что делать.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[2]: Placement new для инициализации примитивного типа в самодельном union?
От: so5team https://stiffstream.com
Дата: 06.10.25 16:48
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Это как std::optional только без маркировки если есть значение или нет?

_NN>Типа https://github.com/akrzemi1/markable ?

Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.

_NN>Кстати memcpy не всегда можно использовать если у нас тип не тривиально копируемый.


Угу.

_NN>А placement new не сработает если нет конструктора копирования.

_NN>В общем, для общего случая там работы хватить

К счастью, общего случая и не нужно. Так что наличие конструкторов для нетривиальных типов можно жестко потребовать.
Re[3]: Placement new для инициализации примитивного типа в самодельном union?
От: _NN_  
Дата: 06.10.25 20:13
Оценка:
Здравствуйте, so5team, Вы писали:

S>Здравствуйте, _NN_, Вы писали:


_NN>>Это как std::optional только без маркировки если есть значение или нет?

_NN>>Типа https://github.com/akrzemi1/markable ?

S>Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.


variant из одного типа и есть optional.
Или там предполагаются разные типы ?

А чего бы не обобщить ?
В С++ всегда обобщаешь, мало ли, что завтра понадобится.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[4]: Placement new для инициализации примитивного типа в самодельном union?
От: rg45 СССР  
Дата: 06.10.25 22:53
Оценка: +1
Здравствуйте, _NN_, Вы писали:

_NN>Или там предполагаются разные типы ?


Два варианта: либо T, либо void_ptr_t.
--
Справедливость выше закона. А человечность выше справедливости.
Re[4]: Placement new для инициализации примитивного типа в с
От: so5team https://stiffstream.com
Дата: 07.10.25 03:56
Оценка:
Здравствуйте, _NN_, Вы писали:

S>>Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.


_NN>variant из одного типа и есть optional.

_NN>Или там предполагаются разные типы ?

Ситуация какая-то такая:
template<typename T>
struct data_chunk_t;

template<typename T>
class my_union_t
{
  using ptr_t = data_chunk_t<T>;

  alignas( std::max( alignof(T), alignof(ptr_t) ) )
  std::array< std::byte, std::max( sizeof(T), sizeof(ptr_t) ) > _content;
  ...
};

template<typename T>
class data_item_t
{
  data_description_t _desc;
  my_union_t<T> _value_or_child_chunk;
  ...
};

template<typename T>
struct data_chunk_t
{
  ...
  std::vector< data_item_t<T> > _data;
  ...
};


Соответственно, в my_union_t может быть либо T, либо указатель на data_chunk_t<T>. Либо, в результате конструктора/оператора перемещения, там может ничего не оказаться. Что именно лежит в data_item_t::_value_of_child_chunk однозначно определяется по содержимому data_item_t::_desc.

_NN>А чего бы не обобщить ?


Потому что приходится решать конкретную задачу, в рамках которой обобщенное программирование задействуется только в той степени, в которой оно нужно для решения задачи. Если бы делалась какая-то библиотека для повторного использования в разных условиях, то тогда был бы и другой разговор.

PS. std::variant не используется потому, что содержимое data_item_t формируется так, чтобы там не было разных лишних данных на современных 64-х битовых платформах. Грубо говоря, сейчас размер data_item_t и выравнивание для него кратны 8. Меняешь my_union на std::variant, появляется, как минимум, лишний байт, который увеличивает размер data_item_t еще на 8 байт (с учетом выравнивания). А т.к. этих data_item_t десятки миллионов, минимум, то лишние 8 байт ведут к десяткам лишних мегабайт занятой ОП.
Отредактировано 07.10.2025 6:11 so5team . Предыдущая версия .
Re[5]: Placement new для инициализации примитивного типа в с
От: _NN_  
Дата: 07.10.25 17:58
Оценка:
Здравствуйте, so5team, Вы писали:

S>PS. std::variant не используется потому, что содержимое data_item_t формируется так, чтобы там не было разных лишних данных на современных 64-х битовых платформах. Грубо говоря, сейчас размер data_item_t и выравнивание для него кратны 8. Меняешь my_union на std::variant, появляется, как минимум, лишний байт, который увеличивает размер data_item_t еще на 8 байт (с учетом выравнивания). А т.к. этих data_item_t десятки миллионов, минимум, то лишние 8 байт ведут к десяткам лишних мегабайт занятой ОП.


Кстати, тогда, лучше использовать обычный массив так как нет гарантий на то, как выглядит std::array.
Скорее всего, и обычно это так, он равносилен массиву, но нет требований стандарта.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[5]: Placement new для инициализации примитивного типа в с
От: _NN_  
Дата: 07.10.25 18:20
Оценка:
Здравствуйте, so5team, Вы писали:

S>>>Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.


Понятно.
То есть, variant с собственной политикой хранения дескриптора.
Поиск в интернете не дал подходящего решения.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[6]: Placement new для инициализации примитивного типа в с
От: so5team https://stiffstream.com
Дата: 08.10.25 06:24
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Кстати, тогда, лучше использовать обычный массив так как нет гарантий на то, как выглядит std::array.

_NN>Скорее всего, и обычно это так, он равносилен массиву, но нет требований стандарта.

Какие-то странные сомнения, если честно.
И непонятно откуда тогда берется рекомендация в C++ Core Guidelines: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#slcon1-prefer-using-stl-array-or-vector-instead-of-a-c-array
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.