有几个来自严格别名的错误,所以我想我会尝试修复所有这些错误。仔细研究了它是什么,有时 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 特定的,但肯定会有一个地方)。
CoCreateInstance 有一个 void** 输出参数,需要显式指针转换。 Direct3D 也有一些类似的东西。
LARGE_INTEGER 是一个 union 体,可能会对不同的成员进行读/写(例如,一些代码可能使用高位/低位,然后其他一些可能会读取 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*
or void*
, 代码完全合法
(除了在第二次使用时缺少类型转换
buffer->GetData()
).只要所有 Actor 都参与
char*
、unsigned char*
或 void*
(忽略 const
限定符),那么编译器需要假设有
是一个可能的别名:当原始指针具有以下之一时
这些类型,它可以通过从
指向目标类型的指针,语言保证
您可以将任何指针转换为这些类型之一,然后返回
原始类型,并恢复相同的值。 (当然,如果
char*
最初不是 uint16_t
,您最终可能会得到
对齐问题,但编译器一般无法知道这一点。)
关于最后一个例子,你没有指明类型
hash.data
,所以不好说;如果是char*
, void*
或者
unsigned char*
,语言保证你的代码
(从技术上讲,前提是 char 指针是由
转换 size_t*
;在实践中,只要
指针充分对齐,指向的字节没有对齐
为 size_t
形成一个陷印值。
一般来说:“类型双关”唯一真正有保障的方式是
通过 memcpy
。否则,指针会强制转换,比如你
做,只要它是从 void*
来的,都是有保证的,
char*
或 unsigned char*
,至少就别名而言
担心的。 (其中之一可能导致对齐
问题,或者在取消引用时访问陷阱值。)
请注意,您可能会从其他人那里获得额外的保证 标准。 Posix 需要这样的东西:
void (*pf)();
*((void**)&pf) = ...
例如,工作。 (通常,强制转换和取消引用 立即将工作,即使使用 g++,如果你什么都不做 else 在别名可能相关的函数中。)
我知道的所有编译器都允许使用union
有时会输入双关语。 (至少有一些,包括
g++,在其他情况下将因 union
的合法使用而失败。
正确处理 union
对编译器编写者来说很棘手
如果 union
不可见。)
关于c++ - 严格的别名似乎不一致,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17722202/