严格的混叠似乎不一致

Wil*_*ill 5 c++ gcc strict-aliasing

有一些来自严格别名的错误,所以我想我会尝试修复所有错误.在详细了解它的情况后,有时GCC似乎没有发出警告,而且有些事情也无法实施.至少根据我的理解,下面的每一个都被打破了 那么我的理解是错误的,是否有正确的方法来完成所有这些事情,或者某些代码是否必须在技术上打破规则并被系统测试很好地覆盖?

这些错误来自一些代码,其中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);
}
Run Code Online (Sandbox Code Playgroud)

将此更改为下面似乎可以解决问题,虽然它仍然涉及演员阵容所以我不确定为什么现在这样做并且没有警告:

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);
}
Run Code Online (Sandbox Code Playgroud)

此外还有许多其他地方似乎没有任何警告

//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);
Run Code Online (Sandbox Code Playgroud)

还有一些......

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

一个非char案例.这没有警告,即使它是坏的,我该如何避免它(两种方式似乎都有效)?

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

看看其他的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*,但在内部分配至少它是一个char)

Jam*_*nze 4

首先,指向char和指向的指针unsigned char几乎不受有关字符串别名的规则的约束;您可以将任何类型的指针转​​换为 achar*或 an unsigned char*,并将指向的对象视为char or的数组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);
}
Run Code Online (Sandbox Code Playgroud)

这里的问题是您试图将 achar*视为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);
}
Run Code Online (Sandbox Code Playgroud)

另一方面,所有转换都涉及char*unsigned char*,两者都可以为任何别名,因此需要编译器来完成这项工作。

至于剩下的,你没有说返回类型 buffer->GetData()是什么,所以很难说。但如果是 char*unsigned char*void*,则代码完全合法(除了第二次使用 时缺少强制转换 buffer->GetData())。只要所有的转换都涉及 a char*、 anunsigned char*或 a void*(忽略const 限定符),那么编译器就需要假设存在可能的别名:当原始指针具有这些类型之一时,它可以通过以下方式创建从指针到目标类型的强制转换,并且该语言保证您可以将任何指针转换为这些类型之一,然后返回到原始类型,并恢复相同的值。(当然,如果char*最初不是 a uint16_t,您最终可能会遇到对齐问题,但编译器通常无法知道这一点。)

对于最后一个例子,你没有指明 的类型 hash.data,所以很难说;如果是char*void*unsigned char*,则该语言保证您的代码(从技术上讲,前提是 char 指针是通过转换 a 创建的size_t*;实际上,前提是该指针充分对齐并且指向的字节不会形成 a 的捕获值size_t)。

一般来说:唯一真正有保证的“类型双关语”方式是通过memcpy. 否则,只要指向或来自 a void*, char*or unsigned char*,至少就别名而言,就可以保证指针强制转换(例如您正在执行的操作)。(其中之一可能会导致对齐问题,或者如果取消引用它,则会导致访问捕获值。)

请注意,您可能会从其他标准获得额外的保证。Posix 需要类似的东西:

void (*pf)();
*((void**)&pf) = ...
Run Code Online (Sandbox Code Playgroud)

例如,去工作。(通常,如果您不在函数中执行与别名相关的任何其他操作,即使使用 g++,强制转换和取消引用也会立即起作用。)

我所知道的所有编译器union有时都会允许使用 for 类型双关。(至少有一些,包括 g++,union在其他情况下会因合法使用而失败。如果 a不可见,正确处理 aunion对于编译器编写者来说是很棘手的。)union