C/C++严格的别名、对象生存期和现代编译器
C/C++ strict aliasing, object lifetime and modern compilers
我对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;
行不会违反规则。
在另一个线程中,成员CortAmmon实际上在响应中提出了同样的观点,并补充说,通过写入活动对象而产生的任何可能的未定义行为,如*f = 1.0f;
,将由标准对"对象生存期"的定义来解释(这对于POD类型来说似乎是微不足道的)。
然而:互联网上有大量证据表明,上面的代码会在现代编译器上产生UB。例如,请参阅此处和此处。
在大多数情况下,论证是编译器可以自由地将&a
和f
视为不会相互混叠,因此可以自由地重新调度指令。
现在最大的问题是,这种编译器行为是否真的是对标准的"过度解释"。
该标准唯一一次专门讨论"别名"是在 3.10.10 的脚注中,其中明确指出这些是管理别名的规则。
正如我之前提到的,我没有看到上述任何代码违反标准,但它会被很多人(可能还有编译器人员)认为是非法的。
我真的很感激在这里澄清一下。
小更新:
正如成员BenVoigt正确指出的那样,在某些平台上,int32_t
可能与float
不一致,因此给定的代码可能违反了"存储足够的对齐和大小"规则。我想声明的是,int32_t
是故意选择的,以与大多数平台上的float
保持一致,并且这个问题的假设是类型确实对齐。
小更新#2:
正如几位成员指出的那样,int32_t b = a;
线可能违反了标准,尽管不是绝对确定的。我同意这一观点,并且不改变问题的任何方面,请读者从我上面的陈述中排除这一行,即没有任何代码违反标准。
你的第三个要点(也许也是第一个)是错误的。
您声明"行float g = *f;
不会出于相同的原因违反规则",其中"只是相同的原因"(有点模糊)似乎指的是"通过其原始类型访问"。但这不是你正在做的事情。您正在通过 float
类型的左值(从表达式 *f
获取)访问int32_t
(名为 a
)。所以你违反了标准。
我也相信(但在这一点上不太确定)存储值是对存储值的访问,因此即使*f = 1.0f;
也违反了规则。
我认为这种说法是不正确的:
行int32_t b = a; 不违反规则,因为我是通过其原始类型访问的。
存储在位置 &a
的对象现在是浮点型,因此您尝试通过错误类型的左值访问浮点数的存储值。
对象生存期和访问的规范中存在一些明显的歧义,但根据我对规范的阅读,这里有一些代码问题。
float* f = (float*)(&a);
这将执行一个reinterpret_cast
,只要float
不需要比int32_t
更严格的对齐,那么您可以将结果值转换回int32_t*
,您将获得原始指针。在任何情况下,都不会另行定义使用结果。
*f = 1.0f;
假设*f
具有a
的别名(并且int32_t
的存储具有适合float
的对齐方式和大小),则上面的行结束了int32_t
对象的生存期,并将float
对象放在其位置:
类型 T对象的生存期从以下时间开始:获得具有类型 T 的正确对齐方式和大小的存储,如果对象具有非平凡的初始化,则其初始化完成。
类型 T 对象的生存期在以下时间结束:[...] 对象占用的存储被重用或释放。
—3.8 对象生存期 [basic.life]/1
我们正在重用存储,但如果int32_t
具有相同的大小和对齐要求,那么似乎float
始终存在于同一个地方(因为存储是"获得"的)。也许我们可以通过将这一行更改为 new (f) float {1.0f};
来避免这种歧义,因此我们知道float
对象的生命周期始于初始化完成或之前。
此外,"访问"并不一定仅仅意味着"读取"。它可以意味着读取和写入。因此,*f = 1.0f;
执行的写入可以通过覆盖存储值来被视为"访问存储值",在这种情况下,这也是混叠冲突。
所以现在假设存在一个浮点对象并且int32_t
对象的生存期已经结束:
int32_t b = a;
此代码通过类型为 int32_t
的 glvalue 访问float
对象的存储值,并且显然是混叠冲突。该程序在 3.10/10 下具有未定义的行为。
float g = *f;
假设int32_t
具有正确的对齐方式和大小要求,并且指针f
是以允许其使用良好定义的方式获得的,那么这应该合法地访问使用 1.0f
初始化的float
对象。
我已经学会了艰难的方式,如果不同时查看 6.5.6,从 C99 标准中引用 6.5.7 是没有帮助的。有关相关报价,请参阅此答案。
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*
例外来查看"错误"类型的数据,否则这是不可能的。
- 在不复制临时对象的情况下延长其生存期
- 结束另一个线程中使用的对象的生存期
- "this"指针的值在对象的生存期内是否恒定?
- 数组对象的生存期是否在重用其元素存储时结束?
- 具有空洞初始化的对象的生存期
- 如何在向量列表初始化时避免对象复制以及如何延长临时的生存期
- 子表达式中临时对象的生存期
- 对临时对象的Const引用不会延长其生存期
- 对象存在与对象生存期不同吗
- 指向对象生存期之外的已分配内存的指针是"invalid pointer[s]"还是"pointer[s] to an object"?
- "std::function"的简单版本:函数对象的生存期?
- 有什么方法可以延长C++中临时对象的生存期吗
- 以延长构造函数外部 QT 对象的生存期
- QML QQmlPropertyList - 包含的对象生存期和'memory rules'
- 在函数调用中C++临时对象的生存期
- std::tie 和元组中返回的对象的生存期
- 是否存在对象存储在其生存期内可能会更改的情况?
- 从“if constexpr”分支扩展对象生存期/范围
- 在函数调用中创建的对象的生存期
- thread_local 和 std::future 对象 - 对象的生存期是多少