严格的别名似乎不一致

时间:2013-07-18 11:28:07

标签: 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);
}

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

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 *,但在内部分配至少它是一个炭)

2 个答案:

答案 0 :(得分:4)

首先,指向charunsigned 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*,该语言保证您的代码 (从技术上讲,只要char指针是由。创建的 转换size_t;在实践中,只要 指针充分对齐,而指向的字节则没有 形成memcpy)的陷印值。

一般来说:唯一真正保证“打字”的方式是 按void*。否则,指针强制转换,例如你 只要它来自char*,就可以保证这样做, unsigned char*void (*pf)(); *((void**)&pf) = ... ,至少就别名而言 关心。 (从其中一个可能导致对齐 问题,或者如果你取消引用它就访问陷阱值。)

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

union
例如,

工作。 (通常,铸造和解除引用 立即工作,即使使用g ++,如果你什么都不做 在别名可能相关的函数中的其他。)

我所知道的所有编译器都允许使用union 在某些时候打字。 (至少有一些,包括 g ++,在其他情况下合法使用union会失败。 正确处理union对编译器编写者来说很棘手 如果{{1}}不可见。)

答案 1 :(得分:0)

char / unsigned char指针不受严格别名规则的约束。

联合技巧在技术上是一个别名错误,但主流编译器无论如何都明确允许它。

因此,您的一些示例是有效的(根据语言,有些是UB,但编译器定义良好)。

但是,是的,有很多代码违反了别名规则。另请注意,MSVC不会根据严格的别名进行优化,因此特别是为Windows编写的代码可能会违反严格的别名规则。