Является ли неопределенным поведением использование тривиального типа без инициализации?
void* mem = malloc(sizeof(uint64_t)*100);
void* num_mem = mem + sizeof(uint64_t)*31;
//does the lifetime of uint64_t starts here:
uint64_t* mynum = reinterpret_cast<uint64_t*>(num_mem); //?
*mynum = 5; //is it UB?
std::cout << *mynum << std::endl; //is it UB?
free(mem);
Я обнаружил, что вызов деструктора тривиальных/POD/агрегатных типов не нужен, но не могу найти то же самое для начала жизни тривиального/POD/агрегатного типа. Мне нужно было звонить new(num_mem) uint64_t;
вместо reinterpret_cast ..
?
Изменяется ли поведение, если это POD или агрегатный объект без конструкторов?
Все нижеследующее следует правилам C++20, хотя неявное создание объекта также считается сообщением о дефекте в более ранних версиях. До С++ 17 значение значений указателя было совсем другим, поэтому обсуждение в ответе и комментариях может не применяться. Все это также строго соответствует тому, что гарантирует стандарт. Конечно, на практике компиляторы могут позволить больше.
Да, если тип и все типы его подобъектов являются неявными типами времени жизни, что является более слабым требованием, чем тривиальность или POD. Это применимо к агрегатным типам, но не обязательно к подобъектам агрегатного типа, и в этом случае их все равно необходимо явно new
ed (например, рассмотрим член struct A { std::string s; };
).
Вы также должны убедиться, что размер и выравнивание памяти подходят. std::malloc
возвращает выравнивание по памяти не менее строгое, чем std::max_align_t
, поэтому это хорошо для любого скалярного типа, но не для типов с чрезмерным выравниванием.
Кроме того, std::malloc
— это одна из немногих функций, специально предназначенных для неявного создания объектов и возврата указателя на один из них. Обычно это не работает для произвольной памяти. Например, если вы используете ячейку памяти как uint64_t
, то неявно созданный объект будет uint64_t
. Последующее приведение к другому типу приведет к нарушению алиасинга.
Детали довольно сложны, и в них легко ошибиться. Гораздо безопаснее явно создавать объекты с помощью new
(что также не требует инициализации значением), а затем использовать указатель, возвращаемый new
, для доступа к объекту.
Также mem + sizeof(uint64_t)*31
является расширением GNU. В стандартном C++ невозможно выполнить арифметические операции с указателями void*
. Вам нужно сначала привести к типу элемента, а затем выполнить арифметику, предполагая, что вы храните в памяти только те же типы. В противном случае все становится немного сложнее.
(Кроме того, вам не хватает проверки нулевого указателя для возвращаемого значения malloc
.)
что касается арифметики указателя, я предполагаю, что указатель на char поможет?
@Arkady Alignment всегда актуален, поскольку невыровненные объекты вообще не могут существовать в стандартном C++ (или, по крайней мере, их время жизни никогда не может начаться). Вы не можете повторно использовать память для разных типов в одном и том же месте без вмешательства new
или какой-либо другой функции, которая указана для неявного создания объектов (например, memcpy
).
@Аркадий Да, а лучше unsigned char
(в стандарте есть мелкая недоработка касаемо char
). Однако это будет означать, что неявное создание объекта создаст массив (unsigned) char
в хранилище, а затем создаст объект целевого типа, вложенный в этот массив. Указатель, возвращенный из malloc
, указывает на элемент char
, а не на вложенный объект. Следовательно, в этом случае вам нужно будет std::launder
указатель после приведения. В любом случае это не меняет того, что я сказал выше: повторное использование одного и того же места в памяти для нескольких типов все равно приведет к UB.
если я вас правильно понял, поскольку только я интерпретирую память, возвращаемую из std::malloc, как тип неявного времени жизни - это начало его жизни. И время жизни благополучно закончится в момент освобождения памяти. Но как только я переосмысливаю память за таким объектом как объект другого типа -- это УБ.
есть ли статья, которую можно прочитать на эту тему, касающуюся С++ 11?
@Arkady Lifetime заканчивается, когда вызывается (псевдо) деструктор, память освобождается или повторно используется для другого объекта. Это справедливо для всех типов (по крайней мере, начиная с C++20). Да, вся идея в том, что malloc
может сделать аналог неявного new
для некоторых типов. Попытка переинтерпретировать этот неявный объект как другой тип является нарушением алиасинга по тем же правилам, что и для явно new
ed объекта.
@Arkady До C++ 20 в стандарте не было ничего, разрешающего неявное создание объекта (но это было исправлено как отчет о дефекте). До того, как значения указателя C++17 определялись их адресом, а не конкретным объектом, на который они указывают, и я думаю, что в результате многие из этих вещей не были точно определены. Я могу дать вам ссылку на документ, в котором представлены изменения, если я его найду.
@Arkady Кажется, на самом деле не было документа, объясняющего изменение C++ 17. Есть только документ с формулировками (open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0137r1.html ). Я предполагаю, что изменение было сделано, потому что оно просто было несовместимым заранее. Но вот статья о неявном создании объектов: open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html
интересно, так что это четко определенный код, пока я не забочусь о выравнивании. Я специально создал буфер размером 800 байт, чтобы избежать какой-либо специализации по типу.