Ответ 1
Некоторые из ваших кодов сомнительны из-за конверсий указателей. Имейте в виду, что в этих случаях reinterpret_cast<T*>(e)
имеет семантику static_cast<T*>(static_cast<void*>(e))
, поскольку используемые типы являются стандартными. (Я бы рекомендовал, чтобы вы всегда использовали static_cast
через cv void*
при работе с хранилищем.)
При закрытом чтении Стандарта предполагается, что во время преобразования указателя в или из T*
предполагается, что действительно существует фактический объект T*
, который трудно выполнить в некоторых фрагментах, даже если "обманывать" благодаря тривиальности вовлеченных типов (подробнее об этом позже). Это было бы, кроме того, потому что...
Алиасинг - это не конверсии указателей.. Это текст С++ 11, в котором описываются правила, которые обычно называются правилами "строгого сглаживания", от 3.10 Lvalues и rvalues [basic.lval ]:
10 Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:
- динамический тип объекта,
- cv-квалифицированная версия динамического типа объекта,
- тип, аналогичный (как определено в 4.4) для динамического типа объекта,
- тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
- тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
- тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
- тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
- a char или неподписанный char тип.
(Это абзац 15 того же предложения и подпункта в С++ 03 с некоторыми незначительными изменениями в тексте с использованием, например, 'lvalue' вместо 'glvalue', поскольку последний является понятием С++ 11. )
В свете этих правил допустим, что реализация предоставляет нам magic_cast<T*>(p)
, которая каким-то образом преобразует указатель на другой тип указателя. Обычно это будет reinterpret_cast
, что в некоторых случаях дает неуказанные результаты, но, как я уже объяснял, это не так для указателей на типы стандартного макета. Тогда верно, что все ваши фрагменты верны (подставляя reinterpret_cast
с помощью magic_cast
), потому что никакие glvalues не связаны с результатами 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 Object lifetime [basic.life]. В абзаце 6 из них приведены очень похожие правила для ссылок на хранилище, а в параграфе 7 объясняется, что происходит с переменными, указателями и ссылками, которые использовались для ссылки на объект после повторного использования его хранилища - я буду ссылаться на все вместе как на 3.8/5- 7.
В этом случае &c
(неявно) преобразуется в void*
, что является одним из правильного использования указателя на хранилище, которое еще не было повторно использовано. Аналогично p
получается из &c
до создания нового int
. Его определение, возможно, может быть перенесено после уничтожения c
, в зависимости от глубины магии реализации, но, конечно же, не после конструкции int
: пункт 7 будет применяться, и это не одна из разрешенных ситуаций. Конструкция объекта short
также полагается на p
, становясь указателем на хранилище.
Теперь, поскольку int
и short
являются тривиальными типами, я не должен использовать явные вызовы деструкторам. Мне также не нужны явные вызовы конструкторам (то есть вызовы обычного, стандартного размещения new, объявленного в <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
тривиально построен на третьей строке).
Как и в случае с Alf, я также рекомендую вам использовать стандартное размещение при использовании хранилища. Пропуск уничтожения для тривиальных типов прекрасен, но если вы встретите *some_magic_pointer = foo;
, вы, вероятно, столкнетесь либо с нарушением 3.8/5-7 (независимо от того, насколько магически этот указатель был получен), либо правил сглаживания. Это означает также сохранение результата нового выражения, так как вы, скорее всего, не сможете повторно использовать волшебный указатель после создания объекта - из-за 3.8/5-7 снова.
Чтение байтов объекта (это означает использование char
или unsigned char
) в порядке, но вы даже не можете использовать reinterpret_cast
или что-либо волшебство вообще. static_cast
через cv void*
, возможно, отлично подходит для работы (хотя мне кажется, что стандарт может использовать там лучшую формулировку).