Еще раз: строгое правило псевдонимов и char *

Чем больше я читаю, тем больше я смущаюсь.

Последний вопрос из близких ближе всего к моему вопросу, но я запутался со всеми словами об объекте жизни и особенно - нормально ли читать или нет.


Чтобы добраться до точки. Исправьте меня, если я ошибаюсь.

Это нормально, gcc не дает предупреждения, и я пытаюсь "читать тип T (uint32_t) через char*":

uint32_t num = 0x01020304;
char* buff = reinterpret_cast< char* >( &num );

Но это "плохо" (также дает предупреждение), и я пытаюсь "по-другому":

char buff[ 4 ] = { 0x1, 0x2, 0x3, 0x4 };
uint32_t num = *reinterpret_cast< uint32_t* >( buff );

Как второй отличается от первого, особенно когда мы говорим о инструкциях по переупорядочению (для оптимизации)? Кроме того, добавление const никак не изменит ситуацию.

Или это просто правильное правило, в котором четко говорится: "Это можно сделать в одном направлении, но не в другом"? Я не мог найти ничего в стандартах (искал это особенно в стандарте С++ 11).

Это то же самое для C и С++ (как я прочитал комментарий, подразумевая, что он отличается для двух языков)?


Я использовал union для "обхода" этого, который по-прежнему кажется НЕ 100% OK, поскольку он не гарантируется стандартом (в котором говорится, что я могу полагаться только на значение, последний раз модифицированный в union).

Итак, прочитав много, я теперь более смущен. Я думаю, что только memcpy является "хорошим" решением?


Похожие вопросы:


ИЗМЕНИТЬ
Ситуация в реальном мире: у меня есть сторонняя библиотека (http://www.fastcrypto.org/), которая вычисляет UMAC, а возвращаемое значение находится в char[ 4 ]. Затем мне нужно преобразовать это в uint32_t. И, кстати, lib часто использует такие вещи, как ((UINT32 *)pc->nonce)[0] = ((UINT32 *)nonce)[0]. Так или иначе.

Кроме того, я спрашиваю, что правильно, а что не так и почему. Не только о переупорядочении, оптимизации и т.д. (Интересно то, что с -O0 нет предупреждений, только с -O2).

И обратите внимание. Я знаю о большой/маленькой ситуации. Это не так. Я действительно хочу проигнорировать это утверждение. "Строгие правила псевдонимов" звучат как что-то действительно серьезное, гораздо более серьезное, чем неправильное утверждение. Я имею в виду - например, доступ к/изменению памяти, которая не должна быть затронута; любой вид UB вообще.

Цитаты из стандартов (C и С++) будут действительно оценены. Я ничего не мог найти о правилах псевдонимов или что-то в этом роде.

Ответы

Ответ 1

Как второй отличается от первого, особенно когда мы говорим о инструкциях по переупорядочению (для оптимизации)?

Проблема заключается в компиляторе, используя правила, чтобы определить, разрешена ли такая оптимизация. Во втором случае вы пытаетесь прочитать объект char[] через несовместимый тип указателя, который является undefined поведением; следовательно, компилятор может переупорядочить чтение и запись (или сделать что-нибудь еще, чего вы не ожидаете).

Как неестественно, как может показаться, вам действительно нужно перестать думать о том, как вы думаете, что компилятор может оптимизировать и просто соблюдать правила.

Или это просто правильное правило, в котором четко говорится: "Это можно сделать в одном направлении, но не в другом"? Я не мог найти ничего в стандартах (искал это особенно в стандарте С++ 11).

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf глава 3.10, пункт 10.

В C99, и я думаю, что также C11, это 6.5 пункт 7.

Оба C и С++ разрешают доступ к любому типу объекта с помощью char * (или, в частности, lvalue типа char). Они не разрешают доступ к объекту char через произвольный тип. Так что да, правило является правилом "один путь".

Я использовал union для "обхода" этого, который по-прежнему кажется НЕ 100% ОК, поскольку он не гарантируется стандартом (который утверждает, что я могу полагаться только на значение, которое в последний раз модифицировано в объединении).

Хотя формулировка стандарта ужасно неоднозначна, в C99 (и за ее пределами) ясно (по крайней мере, с C99 TC3), что цель состоит в том, чтобы разрешить пул через объединение. Тем не менее, вы должны выполнять все обращения через объединение (в частности, вы не можете просто "ввести союз в существование" для целей пиратства).

возвращаемое значение находится в char [4]. Затем мне нужно преобразовать это в uint32_t

Просто используйте memcpy или вручную переместите байты в нужную позицию, если проблема с байтом. Хорошие компиляторы могут в любом случае оптимизировать это (да, даже вызов memcpy).

Ответ 2

Я использовал union для "обхода" этого, который по-прежнему кажется НЕ 100% ОК, поскольку он не гарантируется стандартом (который утверждает, что я могу полагаться только на значение, которое в последний раз модифицировано в объединении).

Эндианс - причина этого. В частности, последовательность байтов 01 00 00 00 может означать 1 или 16 777 216.

Правильный способ сделать то, что вы делаете, - прекратить попытки обмануть компилятор для выполнения преобразования для вас и выполнить преобразование самостоятельно.

Например, если char[4] имеет малоконечный (наименьший байт первым), вы делаете что-то вроде следующего.

char[] buff = new char[4];
uint32_t result = 0;
for (int i = 0; i < 4; i++)
    result = (result << 8) + buff[i];

Это вручную выполняет преобразование между ними и гарантирует, что всегда будет правильно, когда вы выполняете математическое преобразование.

Теперь, если вы быстро делаете это преобразование, может иметь смысл использовать #if и знания вашей архитектуры, чтобы использовать перечисление, чтобы сделать это автоматически, как вы упомянули, но это снова выходит из портативных решений. (Также вы можете использовать что-то вроде этого в качестве резервного, если не можете быть уверены)