Часть вашего кода вызывает сомнения из-за задействованных преобразований указателя. Имейте в виду, что в этих случаях reinterpret_cast<T*>(e)
имеет семантику static_cast<T*>(static_cast<void*>(e))
, потому что задействованные типы имеют стандартную компоновку. (На самом деле я бы рекомендовал вам всегда использовать static_cast
через cv void*
при работе с хранилищем.)
Внимательное прочтение стандарта предполагает, что во время преобразования указателя в или из T*
предполагается, что действительно задействован реальный объект T*
, что трудно выполнить в некоторых фрагментах кода, даже при «обмане» из-за тривиальности. вовлеченных типов (подробнее об этом позже). Впрочем, это было бы неважно, потому что...
Псевдонимы — это не преобразование указателей. Это текст C++11, в котором излагаются правила, которые обычно называют правилами "строгих псевдонимов", из 3.10 Lvalues и rvalues [basic.lval]:
10 Если программа пытается получить доступ к хранимому значению объекта через значение gl, отличное от одного из следующих типов, поведение не определено:
- динамический тип объекта,
- cv-квалифицированная версия динамического типа объекта,
- тип, аналогичный (как определено в 4.4) динамическому типу объекта,
- тип, который является подписанным или беззнаковым типом, соответствующим динамическому типу объекта,
- тип, который является подписанным или беззнаковым типом, соответствующим cv-квалифицированной версии динамического типа объекта,
- агрегатный тип или тип объединения, который включает один из вышеупомянутых типов среди своих элементов или нестатических элементов данных (включая, рекурсивно, элемент или нестатический элемент данных подагрегата или содержащегося объединения),
- тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
- тип char или unsigned char.
(Это параграф 15 того же пункта и подпункта в С++ 03, с некоторыми незначительными изменениями в тексте, например, «lvalue» используется вместо «glvalue», поскольку последнее является понятием C++ 11.)
В свете этих правил давайте предположим, что реализация предоставляет нам magic_cast<T*>(p)
, который «каким-то образом» преобразует указатель в другой тип указателя. Обычно это было бы равно reinterpret_cast
, что в некоторых случаях приводит к неопределенным результатам, но, как я объяснял ранее, это не так для указателей на типы стандартного макета. Тогда совершенно очевидно, что все ваши фрагменты правильны (замена reinterpret_cast
на magic_cast
), потому что никакие значения gl не связаны с результатами magic_cast
.
Вот фрагмент, в котором кажется неправильное использование magic_cast
, но я утверждаю, что он правильный:
// assume constexpr max
constexpr auto alignment = max(alignof(int), alignof(short));
alignas(alignment) char c[sizeof(int)];
// I'm assuming here that the OP really meant to use &c and not c
// this is, however, inconsequential
auto p = magic_cast<int*>(&c);
*p = 42;
*magic_cast<short*>(p) = 42;
Чтобы оправдать мои рассуждения, предположим этот внешне отличающийся фрагмент:
// alignment same as before
alignas(alignment) char c[sizeof(int)];
auto p = magic_cast<int*>(&c);
// end lifetime of c
c.~decltype(c)();
// reuse storage to construct new int object
new (&c) int;
*p = 42;
auto q = magic_cast<short*>(p);
// end lifetime of int object
p->~decltype(0)();
// reuse storage again
new (p) short;
*q = 42;
Этот фрагмент тщательно составлен. В частности, в new (&c) int;
мне разрешено использовать &c
, несмотря на то, что c
было уничтожено в соответствии с правилами, изложенными в параграфе 5 раздела 3.8 Срок службы объекта [basic.life]. Параграф 6 того же дает очень похожие правила для ссылок на хранилище, а параграф 7 объясняет, что происходит с переменными, указателями и ссылками, которые использовались для ссылки на объект после повторного использования его хранилища — я буду называть их в совокупности как 3.8/5- 7.
В этом случае &c
(неявно) преобразуется в void*
, что является одним из правильных способов использования указателя на хранилище, которое еще не использовалось повторно. Точно так же p
получается из &c
до создания нового int
. Его определение, возможно, можно было бы переместить после уничтожения c
, в зависимости от того, насколько глубока магия реализации, но, конечно, не после конструкции int
: будет применяться параграф 7, а это не одна из допустимых ситуаций. Конструкция объекта short
также основана на том, что p
становится указателем на хранилище.
Теперь, поскольку int
и short
являются тривиальными типами, мне не нужно использовать явные вызовы деструкторов. Мне также не нужны явные вызовы конструкторов (то есть вызовы обычного стандартного размещения, объявленного в <new>
). Из 3.8 Срок службы объекта [basic.life]:
1 [...] Время жизни объекта типа T начинается, когда:
- получается хранилище с правильным выравниванием и размером для типа T, и
- если объект имеет нетривиальную инициализацию, его инициализация завершена.
Время жизни объекта типа T заканчивается, когда:
- если T — тип класса с нетривиальным деструктором (12.4), начинается вызов деструктора или
- память, которую занимает объект, используется повторно или освобождается.
Это означает, что я могу переписать код так, что после свертки промежуточной переменной q
я получу исходный фрагмент.
Обратите внимание, что p
нельзя сложить. То есть, следующее определенно неверно:
alignas(alignment) char c[sizeof(int)];
*magic_cast<int*>(&c) = 42;
*magic_cast<short*>(&c) = 42;
Если мы предположим, что объект int
(тривиально) построен со второй строкой, то это должно означать, что &c
становится указателем на хранилище, которое было повторно использовано. Таким образом, третья строка неверна - хотя, строго говоря, из-за 3.8/5-7, а не из-за правил алиасинга.
Если мы этого не предполагаем, то вторая строка является нарушением правил псевдонимов: мы читаем то, что на самом деле является объектом char c[sizeof(int)]
, через glvalue типа int
, который не является одним из допустимых исключение. Для сравнения, *magic_cast<unsigned char>(&c) = 42;
было бы хорошо (мы предположили бы, что объект short
тривиально создается в третьей строке).
Как и Альф, я также рекомендую вам явно использовать новое стандартное размещение при использовании хранилища. Пропускать уничтожение тривиальных типов — это нормально, но встречая *some_magic_pointer = foo;
, вы, скорее всего, сталкиваетесь либо с нарушением 3.8/5-7 (независимо от того, каким волшебным образом был получен этот указатель), либо с нарушением правил псевдонимов. Это также означает сохранение результата нового выражения, поскольку вы, скорее всего, не сможете повторно использовать магический указатель после создания вашего объекта - снова из-за 3.8/5-7.
Однако чтение байтов объекта (это означает использование char
или unsigned char
) прекрасно, и вам даже не нужно использовать reinterpret_cast
или вообще что-то магическое. static_cast
через cv void*
, возможно, подходит для этой работы (хотя мне кажется, что в Стандарте можно было бы использовать более удачную формулировку).
person
Luc Danton
schedule
27.09.2012
i
нарушит строгий псевдоним. - person Mysticial   schedule 27.09.2012memcpy
... - person ildjarn   schedule 27.09.2012new int
делает что-то вродеstatic_cast<int*>(malloc(sizeof(int))
под капотом? - person StackedCrooked   schedule 27.09.2012new int
? РЕДАКТИРОВАТЬ: О, я считаю, чтоmalloc()
является одним из исключений. Я не совсем уверен, что говорит стандарт. Но возвращаясь ко второму случаю, предположим, чтоc
смещено, а процессор не поддерживает смещенный доступ. - person Mysticial   schedule 27.09.2012c
может быть смещено. Я еще не рассматривал это. - person StackedCrooked   schedule 27.09.2012malloc()
к любому из встроенных типов, поскольку он гарантированно будет выровнен со всеми встроенными типами. Но если вы поместитеchar[]
в стек, не гарантируется, что он будет выровнен поint
. Я не знаю, как стандартные слова, хотя. - person Mysticial   schedule 27.09.2012malloc
гарантированно будет максимально выровнен, поэтому вы можете использовать его для любого встроенного типа. Типы с избыточным выравниванием нуждаются в собственной обработке памяти. (Ноmalloc
не имеет ничего общего с каламбуром или алиасингом типов.) - person Kerrek SB   schedule 27.09.2012new char[]
также гарантированно будет максимально выровнен. В противном случае сложно писать контейнеры шаблонов. РЕДАКТИРОВАТЬ: stackoverflow.com/questions/506518/ - person Mysticial   schedule 27.09.2012char
, и наоборот. но я бы использовал новое место размещения вместоreinterpret_cast
... - person Cheers and hth. - Alf   schedule 27.09.2012char
с другим типом. - person Mysticial   schedule 27.09.2012alignas(int) char c[sizeof(int)];
должно быть достаточно, чтобы получить надлежащее хранилище дляint
.std::aligned_storage<sizeof(int), alignof(int)>::type c;
является альтернативой. - person Luc Danton   schedule 27.09.2012placement new
предпочтительнее, потому что он также вызывает конструктор (если он есть). Тем не менее, я предполагаю, что это не помогает с выравниванием? - person StackedCrooked   schedule 27.09.2012memcpy
имеет «чрезвычайно определенное» поведение и, как я читал, может быть оптимизирован в тот же код, что и рассматриваемый здесь случай, я часто просто использую это, вместо того, чтобы ломать себе голову, пытаясь извлечь какой-либо реальный ответ из ужасной комбинации тайные стандартные и бесполезные личные аргументы в таких темах. - person underscore_d   schedule 05.06.2016memcpy
можно использовать для чтения хранилища, записанного как данные неизвестного типа, в хранилище с объявленным типом, но, как правило, нельзя использовать для записи данных, которые были записаны с одним типом таким образом как быть читаемым с помощью другого. Стандарт C++ не указывает явно, как работаетmemcpy
, за исключением того, что он работает так же, как в C. - person supercat   schedule 03.09.2016