8

我对 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;可能违反了标准,尽管不是绝对肯定的。我同意这一观点,并且不改变问题的任何方面,请读者从我上面的声明中排除该行,即没有任何代码违反标准。

4

4 回答 4

5

你的第三个要点是错误的(也许第一个也是)。

您声明“该行float g = *f;并没有出于相同的原因违反规则。”,其中“相同的原因”(有点模糊)似乎是指“通过其原始类型访问”。但这不是你在做什么。您正在通过类型的左值(从表达式获得)访问int32_t(命名)。所以你违反了标准。afloat*f

我也相信(但对此不太确定)存储值是对(那个)存储值的访问,因此甚至*f = 1.0f;违反了规则。

于 2013-09-06T13:53:17.987 回答
2

I think this statement is incorrect:

The line int32_t b = a; doesn't violate the rules because I am accessing through its original type.

The object that is stored at location &a is now a float, so you are attempting to access the stored value of a float through an lvalue of the wrong type.

于 2013-09-06T14:17:47.760 回答
1

对象生存期和访问的规范存在一些明显的歧义,但根据我对规范的阅读,代码存在一些问题。

float* f = (float*)(&a);

这将执行 areinterpret_cast并且只要float不需要更严格的对齐,int32_t您就可以将结果值转换回 anint32_t*并且您将获得原始指针。在任何情况下都没有另外定义使用结果。

*f = 1.0f;

假设*f别名 with a(并且 an 的存储int32_t具有适当的对齐方式和大小 a 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;

float此代码通过类型为 glvalue访问对象的存储值,int32_t显然是一个别名违规。该程序在 3.10/10 下具有未定义的行为。

float g = *f;

假设它int32_t具有正确的对齐和大小要求,并且f已经以允许其使用被良好定义的方式获得了指针,那么它应该合法地访问用 .float初始化的对象1.0f

于 2013-09-06T19:12:49.187 回答
0

我已经了解到从 C99 标准中引用 6.5.7 是没有帮助的,而不看 6.5.6。有关相关报价,请参阅此答案

6.5.6 明确指出对象的类型在某些情况下可以在其生命周期内多次更改。它可以采用最近写入的值的类型。这真的很有用。

我们需要区分“声明类型”和“有效类型”。局部变量或静态全局变量具有声明的类型。我认为,在该对象的整个生命周期中,您都会被这种类型所困扰。您可以使用 a 从对象中读取char *,但不幸的是“有效类型”并没有改变。

但是返回的内存malloc“没有声明的类型”。这将保持正确,直到它是freed。它永远不会有声明的类型,但它的有效类型可以根据 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*异常来查看“错误”类型的数据,否则这是不可能的。

于 2015-07-21T12:52:17.670 回答