Т.е. это такое специализированное хранилище, в котором может быть либо экземпляр типа 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 для инициализации примитивного типа в само
в функции 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. Но если его вызывать при каждом чтении, то компилятор уже ничего оптимизировать не будет, что тоже не очень хорошо.
Здравствуйте, sergii.p, Вы писали:
SP>Чтобы избежать этого нужен std::launder.
Это понятно, std::launder используется.
SP>Но если его вызывать при каждом чтении, то компилытор уже ничего оптимизировать не будет, что тоже не очень хорошо.
Придется мириться, т.к. нет возможности сохранять где-либо возвращенный placement new указатель или даже хранить где-то признак того, что значение создано и может использоваться напрямую.
Re: Placement new для инициализации примитивного типа в самодельном union?
Здравствуйте, Zhendos, Вы писали:
S>>Но вот нужен ли с формальной точки зрения placement new когда мне требуется туда сохранить значение указателя?
Z>По идее для этого как раз придумали std::bit_cast
Может я чего-то не понимаю, конечно, но мне кажется, что это другое.
std::bit_cast нужен чтобы превратить последовательность байт, принадлежащую легальному объекту тривиального типа From в легальный новый объект тривиального типа To (при этом под новый объект типа To автоматически выделяется новое место на стеке).
В моем же случае внутри последовательности байт нужно разместить новый объект примитивного типа и корректно начать его lifetime с точки зрения языка.
Re[3]: Placement new для инициализации примитивного типа в самодельном union?
Здравствуйте, so5team, Вы писали:
S>В моем же случае внутри последовательности байт нужно разместить новый объект примитивного типа и корректно начать его lifetime с точки зрения языка.
Да, для этого в 23 плюсах ввели std::start_lifetime_as. Если 23 нет, placement new будет безопасным вариантом (вместе с ручным вызовом деструктора при пересоздании).
Re[4]: Placement new для инициализации примитивного типа в самодельном union?
Здравствуйте, 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?
Здравствуйте, 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. Однако, не мешало бы получить какие-то подтверждения от знающих людей, чтобы быть уверенным.
Здравствуйте, so5team, Вы писали:
S>Однако, как я понимаю, тогда работа будет выглядеть как-то так:
Создать объект надо один раз. В сторе должно быть создание (через new или sla), а в read просто reinterpret_cast.
Но раз в сторе всё равно запись делается, то толку от sla и нет.
В общем, как у тебя было изначально, так и хорошо.
S>Есть ощущение, что вариант с placement new окажется и лаконичнее, и понятнее.
Здравствуйте, 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.
--
Справедливость выше закона. А человечность выше справедливости.
Тут уже подсказали что и как start lifetime as конечно лучше всего, а если его нет то placement new.
Кстати memcpy не всегда можно использовать если у нас тип не тривиально копируемый.
А placement new не сработает если нет конструктора копирования.
В общем, для общего случая там работы хватить
Может стоит вместо копирования сделать как inplace/ make_optional ?
И передавать параметры как есть дальше, а там уже сам тип лучше тебя знает, что делать.
Здравствуйте, _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?
Здравствуйте, so5team, Вы писали:
S>Здравствуйте, _NN_, Вы писали:
_NN>>Это как std::optional только без маркировки если есть значение или нет? _NN>>Типа https://github.com/akrzemi1/markable ?
S>Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.
variant из одного типа и есть optional.
Или там предполагаются разные типы ?
А чего бы не обобщить ?
В С++ всегда обобщаешь, мало ли, что завтра понадобится.
Здравствуйте, _NN_, Вы писали:
S>>Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.
_NN>variant из одного типа и есть optional. _NN>Или там предполагаются разные типы ?
Соответственно, в 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 байт ведут к десяткам лишних мегабайт занятой ОП.
Здравствуйте, 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.
Скорее всего, и обычно это так, он равносилен массиву, но нет требований стандарта.
Здравствуйте, so5team, Вы писали:
S>>>Скорее как std::variant но без хранения значения index, т.к. что за тип лежит внутри variant-а по косвенным признакам снаружи знает объект, который этим variant-ом и владеет. Собственно, владелец отвечает и за корректное удаление, и за корректное копирование/перемещение.
Понятно.
То есть, variant с собственной политикой хранения дескриптора.
Поиск в интернете не дал подходящего решения.
Здравствуйте, _NN_, Вы писали:
_NN>Кстати, тогда, лучше использовать обычный массив так как нет гарантий на то, как выглядит std::array. _NN>Скорее всего, и обычно это так, он равносилен массиву, но нет требований стандарта.