5

有几个来自严格别名的错误,所以我想我会尝试修复所有这些错误。仔细研究了它是什么,有时 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*,但在分配内部至少它是一个 char)

4

2 回答 2

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);
}

这里的问题是您试图将 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);
}

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

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

关于最后一个例子,你没有指明 的类型 hash.data,所以很难说;如果是char*,void*unsigned char*,语言保证你的代码(技术上,假设 char 指针是通过转换 a 创建的size_t*;在实践中,假设指针充分对齐并且指向的字节不会形成 a 的陷阱值size_t)。

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

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

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

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

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

于 2013-07-18T12:12:40.147 回答
0

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

union 技巧在技术上是一个别名错误,但主流编译器仍然明确允许它。

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

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

于 2013-07-18T11:40:35.157 回答