我面临着关于C ++严格别名规则及其可能含义的困惑。请考虑以下代码:
int main() {
int32_t a = 5;
float* f = (float*)(&a);
*f = 1.0f;
int32_t b = a; // Probably not well-defined?
float g = *f; // What about this?
}
查看C ++规范,3.10.10节,从技术上讲,给定的代码似乎都没有违反给定的“别名规则”:
如果程序试图通过以下类型之一以外的左值访问对象的存储值,则行为未定义:
......一系列合格的访问者类型......
*f = 1.0f;
不会破坏规则,因为无法访问存储值,即我只是通过指针写入内存。我不是从记忆中读书或试图在这里解释一个值。int32_t b = a;
行没有违反规则,因为我正在通过其原始类型进行访问。float g = *f;
不会违反规则。在another thread中,成员CortAmmon实际上在响应中提出相同的点,并添加通过写入活动对象而产生的任何可能的未定义行为,如*f = 1.0f;
中所示,将由标准的“对象生命周期”定义来解释(对于POD类型来说似乎是微不足道的。)
但是:互联网上有很多的证据表明上面的代码会在现代编译器上产生UB。例如,请参阅here和here
在大多数情况下,论证是编译器可以自由地将&a
和f
视为彼此没有别名,因此可以自由重新安排指令。
现在最大的问题是,这种编译器行为是否实际上是对标准的“过度解释”
唯一一次标准谈论“混叠”的唯一一次是在3.10.10的脚注中,其中明确指出那些是控制混叠的规则。
正如我前面提到的,我没有看到任何上述代码违反了标准,但是很多人(可能还有编译人员)认为它是非法的。
我真的非常感谢这里的澄清。
小更新:
正如BenVoigt成员正确指出的那样,int32_t
可能在某些平台上与float
不一致,因此给定的代码可能违反了“存储足够的对齐和大小”规则。我想说明在大多数平台上有意选择int32_t
与float
一致,并且这个问题的假设是类型确实对齐。
小更新#2:
正如一些成员指出的那样,int32_t b = a;
这一行可能违反了标准,尽管没有绝对的确定性。我同意这一观点,并且不改变问题的任何方面,要求读者从我上面的陈述中排除这一行,即没有任何代码违反标准。
答案 0 :(得分:5)
你的第三个要点(也许是第一个要点)错了。
你说“行float g = *f;
并没有因为同样的原因违反规则。”,“只是同样的原因”(有点模糊)似乎是指“通过原始类型访问” 。但这不是你正在做的事情。您正在通过类型为int32_t
的左值(从表达式a
获取)访问float
(名为*f
)。所以你违反了标准。
我也相信(但在这一点上不太确定)存储值是对(存储)值的访问,因此即使*f = 1.0f;
违反了规则。
答案 1 :(得分:2)
我认为这种说法不正确:
行int32_t b = a;不违反规则,因为我通过其原始类型访问。
存储在位置&a
的对象现在是一个浮点数,因此您试图通过错误类型的左值来访问浮点数的存储值。
答案 2 :(得分:1)
对象生命周期和访问的规范存在一些明显的含糊之处,但根据我对规范的阅读,代码存在一些问题。
float* f = (float*)(&a);
这会执行reinterpret_cast
,只要float
不需要比int32_t
更严格的对齐,那么您可以将结果值转换回int32_t*
,您将获得原始指针。在任何情况下都不会使用结果。
*f = 1.0f;
假设*f
的{{1}}别名(并且a
的存储空间具有int32_t
的适当对齐和大小),则上述行将终止float
对象并在其位置放置一个int32_t
对象:
类型T的对象的生命周期开始于:获得具有适当对齐和类型T大小的存储,并且如果对象具有非平凡的初始化,则其初始化完成。
类型T的对象的生命周期在以下情况下结束:[...]对象占用的存储空间被重用或释放。
-3.8对象生命周期[basic.life] / 1
我们正在重复使用存储空间,但如果float
具有相同的大小和对齐要求,则int32_t
似乎始终存在于同一位置(因为存储已“获得”)。也许我们可以通过将此行更改为float
来避免这种歧义,因此我们知道new (f) float {1.0f};
对象的生命周期始于初始化完成时或之前。
此外,“访问”并不一定仅仅意味着“阅读”。它可以表示读取和写入。因此,float
执行的写操作可以被认为是“通过写入来访问存储的值”,在这种情况下,这也是一个别名冲突。
所以现在假设存在一个浮点对象并且*f = 1.0f;
对象的生命周期已经结束:
int32_t
此代码通过类型为int32_t b = a;
的glvalue访问float
对象的存储值,显然是一种锯齿违规。该程序在3.10 / 10下具有未定义的行为。
int32_t
假设float g = *f;
具有正确的对齐和大小要求,并且指针int32_t
已经以允许明确定义其使用的方式获得,那么这应该合法地访问{{ 1}}使用f
初始化的对象。
答案 3 :(得分:0)
我已经学会了很难从C99标准中引用6.5.7而没有看到6.5.6的帮助。有关相关引号,请参阅this answer。
6.5.6清楚地表明,在某些情况下,对象的类型在其生命周期内可以多次改变。它可以采用最近写入的值的类型。这非常有用。我们需要区分"声明类型"和"有效类型"。局部变量或静态全局变量具有声明的类型。我认为,对于该对象的生命周期,你会被这种类型所困扰。您可以使用char *
从对象读取,但是"有效类型"不幸的是,它并没有改变。
但malloc
返回的内存有"没有声明的类型"。这将保持为真,直到free
d。它永远不会有声明的类型,但它的有效类型可以根据6.5.6改变,总是采用最近写入的类型。
所以,这是合法的:
int main() {
void * vp = malloc(sizeof(int)+sizeof(float)); // it's big enough,
// and malloc will look after alignment for us.
int32_t *ap = vp;
*ap = 5; // make int32_t the 'effective type'
float* f = vp;
*f = 1.0f; // this (legally) changes the effective type.
// int32_t b = *ap; // Not defined, because the
// effective type is wrong
float g = *f; // OK, because the effective type is (currently) correct.
}
因此,基本上,写入malloc
- ed空间是改变其类型的有效方法。但我想这并不能让我们通过镜头来看待预先存在的镜头"一种新型的,可能很有趣;除非,我认为,我们使用各种char*
例外来查看错误"的数据,这是不可能的。类型。