C++中字符指针的严格别名规则。

4

我正在尝试理解严格别名规则。通过阅读什么是严格别名规则的回答,我有一些进一步的问题。这里我借用了那篇文章中的例子:

struct Msg { uint32_t a, b; }
void SendWord(uint32_t);
void SendMessage(uint32_t* buff, uint32_t buffSize)
{
  for (uint32_t i = 0; i < buffSize; ++i) SendWord(buff[i]);
}

现在考虑以下代码 (A):

uint32_t *buff = (uint32_t*)malloc(sizeof(Msg));
std::memset(buff, 0, sizeof(Msg));
Msg *msg = (Msg*)buff; // Undefined behavior.
for (uint32_t i = 0; i < 10; ++i)
{
  msg->a = i;
  msg->b = i + 1;
  SendMessage(buff, 2);
}
free(buff);

对于上述代码,作者解释了由于未定义行为可能会发生的情况。发送的消息可能包含全0:在优化过程中,编译器可能假设 msgbuff 指向不同的内存块,并决定对 msg 的写入不会影响到 buff

但是下面的代码(B)又如何呢:

uint32_t *buff = (uint32_t*)malloc(sizeof(Msg));
std::memset(buff, 0, sizeof(Msg));
unsigned char *msg = (unsigned char*)buff; // Compliant.
for (uint32_t i = 0; i < sizeof(Msg); ++i)
{
  msg[i] = i + 1;
  SendMessage(buff, 2);
}
free(buff);

发送的消息是否可以保证按照预期进行(就好像关闭了严格别名编译标志一样)?如果是这样,那么仅仅因为*char (msg)指向与buff相同的位置,编译器应该并将会注意到它,并且避免在(A)中可能出现的上述优化吗?

然而,我又读了一篇针对帖子Strict aliasing rule and 'char *' pointers的另一个评论,该评论表示使用*char指针写入所引用的对象是未定义行为。因此,代码(B)仍然可能导致类似的意外行为吗?


1
在此处阅读类型别名。特别允许 - “AliasedType是std::byte(自C++17起),char或unsigned char:这允许将任何对象的对象表示形式作为字节数组进行检查。” - Richard Critten
@RichardCritten 看起来这似乎可以成为一个完整的答案。 - cigien
@RichardCritten 我明白了。所以完整的句子是“每当尝试通过类型为AliasedType的glvalue读取或修改DynamicType类型对象的存储值时,除非以下情况之一成立,否则行为未定义:(3)AliasedType是std :: byte,(自C++17起)char或unsigned char”。这意味着写入该char指针是有效的,对吗?如果您想详细说明,我会接受您的答案。 - user2961927
你所链接的答案仅适用于C语言(而且,是错误的!);在C和C++中,strict aliasing规则是不同的。 - M.M
@M.M,你说的错误是指我帖子中的第二个链接吗? - user2961927
我所说的“错误”,是指 https://dev59.com/43VD5IYBdhLWcg3wE3Ro#99010 中的答案(该答案中的第一个代码示例在 C 语言中是正确的)。 - M.M
1个回答

1
首先,回答https://dev59.com/43VD5IYBdhLWcg3wE3Ro#99010是适用于C语言的(实际上是完全错误的,但这是另一件事),你提问的是关于C++的,严格别名规则在C和C++中设置不同。因此,该答案与此问题无关。
在C++20之前,由于赋值运算符的行为仅定义为写入对象的情况,因此您代码的两个版本都会导致未定义的行为(通过遗漏)。C++中的malloc函数分配空间但不在其中创建对象。应使用其他构造来处理此任务,例如new或更高级别的容器,这些构造既分配空间又在空间中创建对象以供编写。
尝试在C++严格别名规则的上下文中分析此代码(C++20之前)是不可能的,因为规则的定义是关于访问对象的存储值,但在此代码中没有访问任何对象,因为没有创建任何对象。
自C++20以来,有一个新的规定(N4860 [intro.object]/10),如果存在这样的对象组合,可以使代码定义良好,则对象可以通过赋值运算符隐式创建。 (否则行为仍然未定义)。
根据对象模型的这个变化,您的两个代码示例都是定义良好的。在(A)中,可以在空间中隐式创建uint32_t对象,在(B)中可以在空间中隐式创建unsigned char对象。由于您的代码不会以一种类型写入,然后以不同类型读取,因此不存在别名违规的可能性。
各种类型的中间指针(例如buff)对严格别名(无论使用哪种语言)没有影响-该规则严格关于如何读取和写入空间;而不是关于我们如何到达该空间。

我认为这不正确。隐式创建的对象需要是一个 Msg(以及其子对象)。否则,msg->a = i 将会是未定义行为。将其作为 uint32_t* 传递仅允许是因为 Msg 是标准布局且第一个成员是 uint32_t,因此指针可互换。但是,在 buff[i] 中进行指针算术运算以到达第二个成员是不允许的。在 (B) 中,创建的对象需要是一个(数组)uint32_t,但是通过 unsigned char 访问将被允许。 - user17732522
@user17732522,实际的代码示例A和B中并没有buff[i] - M.M
你提出了一个老问题,即 msg->a 是否需要所有的 *msg 存在,还是只需要正确类型的元素存在。尽管我承认所有的 *msg 必须存在的观点更为普遍,但标准从未清楚地指明这一点。 - M.M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接