Было несколько ошибок от строгого алиасинга, и я подумал, что я попытаюсь исправить их все. Изучив некоторые детали того, что это такое, иногда кажется, что 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, но наверняка есть где-нибудь).
CoCreateInstance Имеет void ** выходной параметр, требующий явного приведения указателя. В Direct3D есть и такие.
LARGE_INTEGER — это объединение, которое, скорее всего, будет выполнять чтение / запись для разных членов (например, один код может использовать высокий / низкий уровень, тогда как другой может читать int64).
Я вспоминаю, что реализация CPython весьма удачно встраивает PyObject * в кучу других вещей, которые в начале имеют одинаковую структуру памяти.
Я видел много реализаций хешей, которые преобразуют входной буфер в uint32_t *, а затем, возможно, используют uint8_t для обработки 1-3 байтов в конце.
Практически каждая реализация распределителя памяти, которую я видел, использует char * или unsigned char *, которые затем должны быть преобразованы в нужный тип (возможно, через возвращенный void *, но внутренне для выделения, по крайней мере, это был символ).
Во-первых, указатели на 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
не видно.)
char
/unsigned char
указатели освобождены от строгих правил наложения имен.
Уловка объединения технически является ошибкой псевдонима, но основные компиляторы явно разрешают ее в любом случае.
Так что некоторые из ваших примеров верны (а некоторые — UB в соответствии с языком, но хорошо определены компилятором).
Но да, есть много кода, который нарушает правила псевдонимов. Также обратите внимание, что MSVC не выполняет оптимизацию на основе строгого алиасинга, и поэтому особенно код, написанный для Windows, может быть подвержен нарушению строгих правил алиасинга.