这真的违反了严格别名规则吗?

36

当我使用g++编译这个示例代码时,会收到以下警告:

警告:解引用类型转换的指针将违反严格别名规则[-Wstrict-aliasing]

代码如下:

#include <iostream>

int main() 
{
   alignas(int) char data[sizeof(int)];
   int *myInt = new (data) int;
   *myInt = 34;

   std::cout << *reinterpret_cast<int*>(data);
}
在这种情况下,data 不是别名 int 吗?那么将其强制转换回 int 就不会违反严格别名规则了,对吧?还是我漏掉了什么?
编辑:奇怪的是,当我像这样定义 data 时:
alignas(int) char* data = new char[sizeof(int)];
编译器警告消失了。在严格别名下,堆栈分配有影响吗?它是一个char[]而不是char*,这意味着它实际上不能与任何类型别名吗?

编译器警告消失了。在严格别名下,堆栈分配是否有影响?它是char[]而不是char*,这是否意味着它实际上无法与任何类型别名?


3
@molbdnilo char* 可以始终作为别名。 - Shafik Yaghmour
@ShafikYaghmour 当然可以,我怎么可能会忘记呢? - molbdnilo
3
你可能想考虑使用 std::aligned_storage 来处理这个问题:http://en.cppreference.com/w/cpp/types/aligned_storage。 - mattnewport
@mattnewport 谢谢,我会研究一下。目前我只是使用 #pragma pack(1) 来使所有内容的对齐方式为 1,但这可能会给我带来更多的自由。 - Red Alert
3
自gcc 7.2起为什么警告完全消失了? LIVE(https://godbolt.org/g/ci5dKj) - sandthorn
显示剩余4条评论
3个回答

24

警告是完全有理由的。指向 data 的已经损坏的指针 并不指向类型为 int 的对象,对其进行强制类型转换也无法改变这一点。请参见 [basic.life]/7

如果在一个对象的生命周期结束后,在释放或重用该对象所占用的存储之前,在原始对象所占用的存储位置上创建了一个新对象,则 指向原始对象的指针、引用原始对象的引用或者指向原始对象的名称将自动引用新对象,一旦新对象的生命周期开始,就可以用来操作新对象,如果
(7.1) — [..]
(7.2) — 新对象与原对象具有相同的类型(不考虑最顶层的cv限定符)

新对象不是 char 数组,而是 int。正式化指针概念的 P0137 添加了 launder 函数:

[:如果不满足这些条件,则可以通过调用 std::launder(18.6 [support.dynamic])从表示其存储地址的指针获取指向新对象的指针。—注解结束]

也就是说,您的代码片段可以这样更正:

std::cout << *std::launder(reinterpret_cast<int*>(data));

或者只需从就地new的结果初始化一个新指针,这也会消除警告。


2
@supercat:memcpy和memmove的相对效率如何与此问答有关? - Kevin
3
自GCC 7.2版本以来,为什么警告完全消失了?现场展示(https://godbolt.org/g/ci5dKj) - sandthorn
1
@curiousguy 不是的。 - Columbo
1
@curiousguy 嗯,是的吗?假设一个非活动成员是活着的确是未定义的。即使直到最近才明确表达这一点,也并不意味着委员会旨在制定的一致规则系统只允许大量垃圾。现在已经被解释清楚了。 - Columbo
1
@Barry,你肯定是想在转换之后进行“洗数据”(launder),而不是在之前吧? - Columbo
显示剩余17条评论

0

*myInt = 34; 这个表达式是正确的,因为 data 提供了 int 类型对象的存储空间,而 myInt 是指向 int 类型对象的指针。因此,解引用这样的指针可以访问 int 类型的对象。

对于表达式 *reinterpret_cast<int*>(data);,它将违反严格的指针别名规则。 首先,data 应用了数组到指针的转换,结果是指向 data 的初始元素的指针,也就是说,reinterpret_cast<int*> 的操作数是指向 data 的主题的指针。
根据以下规则:

如果一个对象在与成员子对象或数组元素 e 相关联的存储中创建,则如果满足以下条件,所创建的对象是 e 所包含的对象的子对象:

  • e 所包含的对象的生命周期已经开始但尚未结束;
  • 新对象的存储正好覆盖与 e 相关联的存储位置;
  • 新对象与 e 是相同类型(忽略 cv 限定符)。

int 类型的对象不满足这些规则中的任何一个。因此,reinterpret_cast<int*> 的操作数不是指向与 int 类型对象互相转换的对象的指针。所以,reinterpret_cast<int*> 的结果不是指向 int 类型对象的指针。

如果程序试图通过非以下类型的 glvalue 访问对象的存储值,行为未定义:

  • 对象的动态类型。
  • [...]

-5

改成什么样子呢?

std::cout << *reinterpret_cast<int*>(data);

int *tmp   = reinterpret_cast<int*>(data);
std::cout << *tmp;

?

在我的情况下,它消除了警告。

6
根本问题仍然存在。 - HolyBlackCat
@curiousguy 我不是母语人士,所以也许“underlying”不是正确的词。重点是警告存在是因为OP的代码导致未定义行为(如其他答案中所解释的),正确的做法是更改代码以消除该UB,而不仅仅是像这个答案那样试图消除警告。 - HolyBlackCat
@curiousguy 破坏严格别名规则是未定义行为。 - HolyBlackCat
@HolyBlackCat,"破坏严格别名"这种说法根本不存在。这是一个荒谬的想法。如果真有这种情况,那也不可能存在。 - curiousguy
7
如果您不同意其他回答,可以发布自己的回答,并解释为什么OP代码的行为是明确定义的,以及为什么警告是不正确的?“没有所谓的“违反严格别名”。这是一个荒谬的想法。” 您应该阅读《什么是严格别名规则?》(What is the strict aliasing rule?)。 - HolyBlackCat
显示剩余3条评论

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