Может ли это привести к неопределенному поведению?
uint8_t storage[4];
// We assume storage is properly aligned here.
int32_t* intPtr = new((void*)storage) int32_t(4);
// I know this is ok:
int32_t value1 = *intPtr;
*intPtr = 5;
// But can one of the following cause UB?
int32_t value2 = reinterpret_cast<int32_t*>(storage)[0];
reinterpret_cast<int32_t*>(storage)[0] = 5;
char
есть особые правила для строгого алиасинга. Если я использую char
вместо uint8_t
это все еще неопределенное поведение? Что еще меняется?
Как отметил член DeadMG, reinterpret_cast
зависит от реализации. Если я использую бросок в стиле C (int32_t*)storage
вместо этого, что изменится?
Указатель, возвращаемый путем размещения new, может быть таким же вызывающим UB, как и любой другой указатель, когда в него вносятся соображения алиасинга. Вы несете ответственность за то, чтобы память, в которую вы поместили объект, не была псевдонимом того, чем не должна быть.
В этом случае вы не можете предполагать, что uint8_t
это псевдоним для char
и поэтому применяются специальные правила наложения имен. Кроме того, было бы довольно бессмысленно использовать массив uint8_t
скорее, чем char
так как sizeof()
с точки зрения char
не uint8_t
, Вы должны сами рассчитать размер.
К тому же, reinterpret_cast
Эффект полностью определяется реализацией, поэтому код, безусловно, не имеет четко определенного значения.
Для реализации низкоуровневых неприятных взломов памяти исходная память должна быть наложена только на char*
, void*
, а также T*
, где T
является конечным типом назначения — в этом случае int
плюс все, что вы можете получить от T*
например, если T
является производным классом, и вы конвертируете указатель этого производного класса в указатель на базу. Все остальное нарушает строгий псевдоним и привет носовым демонам.
Ваша версия, использующая обычное размещение новых, действительно в порядке.
Есть интерпретация1 пп. 3.8 / 1 и 3.8 / 4, где объекты тривиальных типов могут «исчезать» и «появляться» по требованию. Это не бесплатный проход, который позволяет игнорировать правила наложения имен, поэтому обратите внимание:
std::uint16_t storage[2];
static_assert( /* std::uint16_t is not a character type */ );
static_assert( /* storage is properly aligned for our purposes */ );
auto read = *reinterpret_cast<std::uint32_t*>(&storage);
// At this point either we’re attempting to read the value of an
// std::uint16_t object through an std::uint32_t glvalue, a clear
// strict aliasing violation;
// or we’re reading the indeterminate value of a new std::uint32_t
// object freshly constructed in the same storage without effort
// on our part
Если, с другой стороны, вы поменялись приведениями во втором фрагменте (то есть переосмыслите и напишите сначала), вы тоже не совсем в безопасности. В то время как под интерпретацией вы можете оправдать запись на новый std::uint32_t
объект, который неявно использует хранилище, последующее чтение имеет вид
auto value2 = *reinterpret_cast<int32_t*>(storage);
и §3.8 / 5 говорит (акцент мой и чрезвычайно актуален):
[…] После окончания срока службы объекта и до повторного использования хранилища, занимаемого объектом или освобожден, любой указатель, который ссылается на место хранения, где объект будет или был расположен, может использоваться, но только ограниченным образом. […] Такой указатель относится к выделенному хранилищу (3.7.4.2) и использует указатель, как если бы указатель имел тип void*
, четко определено.
§3.8 / 6 — это то же самое, но в форме ссылки / блочности (возможно, более уместно, поскольку здесь мы повторно используем имя, а не указатель, но абзац сложнее понять вне контекста). Также см. §3.8 / 7, который дает некоторую ограниченную свободу действий, которую я не считаю применимой в вашем случае.
Чтобы упростить ситуацию, остающаяся проблема заключается в следующем:
T object;
object.~T();
new (&object) U_thats_really_different_from_T;
&object; // Is this allowed? What does it mean?
static_cast<void*>(&object); // Is this?
Как это происходит, если тип хранилища включает в себя простой или неподписанный символьный тип (например, ваш storage
действительно имеет тип unsigned char[4]
) тогда я бы сказал, что у вас есть основание для обоснования формирования указателя / ссылки на хранилище нового объекта (возможно, будет переосмыслено позже). Смотрите, например Again 5 и 6 снова, которые имеют явное условие escape для формирования указателя / reference / glvalue и §1.8 Объектная модель C ++ это описывает, как объект включает в себя массив байтов. Правила, регулирующие преобразование указателя, должны быть простыми и неоспоримыми (по крайней мере, для сравнения…).
1: трудно оценить, насколько хорошо это толкование получено в сообществе — я видел его в списке рассылки Boost, где к нему был некоторый скептицизм