C / C ++严格别名,对象生命周期和现代编译器

时间:2013-09-06 13:48:50

标签: c++ memory compiler-construction strict-aliasing type-punning

我面临着关于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。例如,请参阅herehere 在大多数情况下,论证是编译器可以自由地将&af视为彼此没有别名,因此可以自由重新安排指令。

现在最大的问题是,这种编译器行为是否实际上是对标准的“过度解释” 唯一一次标准谈论“混叠”的唯一一次是在3.10.10的脚注中,其中明确指出那些是控制混叠的规则。
正如我前面提到的,我没有看到任何上述代码违反了标准,但是很多人(可能还有编译人员)认为它是非法的。

我真的非常感谢这里的澄清。

小更新:
正如BenVoigt成员正确指出的那样,int32_t可能在某些平台上与float不一致,因此给定的代码可能违反了“存储足够的对齐和大小”规则。我想说明在大多数平台上有意选择int32_tfloat一致,并且这个问题的假设是类型确实对齐。

小更新#2:
正如一些成员指出的那样,int32_t b = a;这一行可能违反了标准,尽管没有绝对的确定性。我同意这一观点,并且不改变问题的任何方面,要求读者从我上面的陈述中排除这一行,即没有任何代码违反标准。

4 个答案:

答案 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*例外来查看错误"的数据,这是不可能的。类型。