Строгий псевдоним кажется несовместимым

Было несколько ошибок от строгого алиасинга, и я подумал, что я попытаюсь исправить их все. Изучив некоторые детали того, что это такое, иногда кажется, что GCC не выдает предупреждение, а также то, что некоторые вещи невозможно реализовать. По крайней мере, по моему пониманию, все ниже сломано. Так что мое понимание неверно, есть ли правильный способ сделать все это, или какой-то код просто должен технически нарушить правило и быть хорошо охваченным системными тестами?

Ошибки были из некоторого кода, в котором были смешаны char и unsigned char buffers, например как ниже:

size_t Process(char *buf, char *end)
{
char *p = buf;
ProcessSome((unsigned char**)&p, (unsigned char*)end);
//GCC decided p could not be changed by ProcessSome and so always returned 0
return (size_t)(p - buf);
}

Изменение этого значения на приведенное ниже, похоже, решило проблему, хотя оно все еще включает приведение, поэтому я не уверен, почему это теперь работает и не содержит предупреждений:

size_t Process(char *buf, char *end)
{
unsigned char *buf2 = (unsigned char *)buf;
unsigned char *p = buf2;
unsigned char *end2 = (unsigned char*)end;
ProcessSome(&p, end2);
return (size_t)(p - buf2);
}

Также есть множество других мест, которые, кажется, работают без предупреждений

//contains a unsigned char* of data. Possibly from the network, disk, etc.
//the buffer contents itself is 8 byte aligned.
const Buffer *buffer = foo();
const uint16_t *utf16Text = (const uint16_t*)buffer->GetData();//const unsigned char*
//... read utf16Text. Does not even seem to ever be a warning//also seems to work fine
size_t len = CalculateWorstCaseLength(...);
Buffer *buffer = new Buffer(len * 2);
uint16_t *utf16 = (uint16_t*)buffer->GetData();//unsigned char*
len = DoSomeProcessing(utf16, len, ...);
buffer->Truncate(len * 2);
send(buffer);

И некоторые с …

struct Hash128
{
unsigned char data[16];
};
...
size_t operator ()(const Hash128 &hash)
{
return *(size_t*)hash.data;//warning
}

Случай не символ. Это не имеет предупреждения, и даже если это плохо, как мне избежать этого (кажется, оба способа работают)?

int *x = fromsomewhere();//aligned to 16 bytes, array of 4
__m128i xmm = _mm_load_si128((__m128*i)x);
__m128i xmm2 = *(__m128i*)x;

Глядя на другие API, кажется, есть и другие случаи, которые, по моему пониманию, нарушают правило (не сталкивались со спецификой Linux / GCC, но наверняка есть где-нибудь).

  1. CoCreateInstance Имеет void ** выходной параметр, требующий явного приведения указателя. В Direct3D есть и такие.

  2. LARGE_INTEGER — это объединение, которое, скорее всего, будет выполнять чтение / запись для разных членов (например, один код может использовать высокий / низкий уровень, тогда как другой может читать int64).

  3. Я вспоминаю, что реализация CPython весьма удачно встраивает PyObject * в кучу других вещей, которые в начале имеют одинаковую структуру памяти.

  4. Я видел много реализаций хешей, которые преобразуют входной буфер в uint32_t *, а затем, возможно, используют uint8_t для обработки 1-3 байтов в конце.

  5. Практически каждая реализация распределителя памяти, которую я видел, использует char * или unsigned char *, которые затем должны быть преобразованы в нужный тип (возможно, через возвращенный void *, но внутренне для выделения, по крайней мере, это был символ).

5

Решение

Во-первых, указатели на char и к unsigned char довольно много
освобожден от правил, касающихся псевдонимов строк; вам разрешается
преобразовать любой тип указателя в char* или unsigned
char*
и посмотрите на указанный объект как массив char
или же unsigned char, Теперь, что касается вашего кода:

size_t Process(char *buf, char *end)
{
char *p = buf;
ProcessSome((unsigned char**)&p, (unsigned char*)end);
//GCC decided p could not be changed by ProcessSome and so always returned 0
return (size_t)(p - buf);
}

Проблема в том, что вы пытаетесь взглянуть на char* как будто
это был unsigned char*, Это не гарантировано. Дано
что актерский состав хорошо виден, g ++ немного туповат
о том, чтобы не выключать строгий анализ псевдонимов
автоматически, но технически, это охватывается стандартом.

В

size_t Process(char *buf, char *end)
{
unsigned char *buf2 = (unsigned char *)buf;
unsigned char *p = buf2;
unsigned char *end2 = (unsigned char*)end;
ProcessSome(&p, end2);
return (size_t)(p - buf2);
}

с другой стороны, все преобразования включают char* а также
unsigned char*, оба из которых могут иметь псевдоним, так что
компилятор требуется, чтобы сделать эту работу.

Что касается остального, вы не говорите, что тип возврата
buffer->GetData() есть, так что сложно сказать. Но если это
char*, unsigned char* или же void*код полностью легален
(за исключением отсутствующего приведения во втором использовании
buffer->GetData()). Пока все забросы включают
char*, unsigned char* или void* (без учета const
квалификаторов), то компилятор должен предполагать, что
возможный псевдоним: когда исходный указатель имеет один из
эти типы, он мог быть создан с помощью приведения из
указатель на целевой тип, а язык гарантирует, что
Вы можете преобразовать любой указатель в один из этих типов, и обратно
исходный тип, и восстановить то же значение. (Конечно, если
char* изначально не был uint16_t, вы можете в конечном итоге
проблемы с выравниванием, но компилятор обычно не может этого знать.)

Что касается последнего примера, вы не указываете тип
hash.dataтак сложно сказать; если это char*, void* или же
unsigned char*, язык гарантирует ваш код
(технически, при условии, что указатель на символ был создан
преобразование size_t*; на практике, при условии, что
указатель достаточно выровнен, а указанные байты не
сформировать значение захвата для size_t).

В целом: единственный действительно гарантированный способ «наказания за тип»
от memcpy, В противном случае, указатель бросает, например, вы
гарантированно до тех пор, пока void*,
char* или же unsigned char*по крайней мере, насколько псевдоним
обеспокоен. (От одного из них может привести к выравниванию
проблемы или доступ к значению перехвата, если вы разыменовали его.)

Обратите внимание, что вы можете получить дополнительные гарантии от других
стандарты. Posix требует что-то вроде:

void (*pf)();
*((void**)&pf) = ...

работать, например. (Как правило, приведение и разыменование
сразу будет работать, даже с g ++, если ты ничего не делаешь
еще в функции, где псевдонимы могут иметь значение.)

И все известные мне компиляторы позволят использовать union за
типа удары, иногда. (И хотя бы некоторые, в том числе
g ++, потерпит неудачу при легальном использовании union в других случаях.
Правильно обрабатывать union сложно для автора компилятора
если union не видно.)

4

Другие решения

char/unsigned char указатели освобождены от строгих правил наложения имен.

Уловка объединения технически является ошибкой псевдонима, но основные компиляторы явно разрешают ее в любом случае.

Так что некоторые из ваших примеров верны (а некоторые — UB в соответствии с языком, но хорошо определены компилятором).

Но да, есть много кода, который нарушает правила псевдонимов. Также обратите внимание, что MSVC не выполняет оптимизацию на основе строгого алиасинга, и поэтому особенно код, написанный для Windows, может быть подвержен нарушению строгих правил алиасинга.

0

По вопросам рекламы [email protected]