【问题标题】:Is it legal to modify a dynamically-allocated `const` object through a re-used non-`const` name?通过重用的非 const 名称修改动态分配的 const 对象是否合法?
【发布时间】:2014-04-02 10:08:11
【问题描述】:

考虑以下程序:

#include <iostream>

int main()
{
   int x = 0;
   const int* px = new (&x) const int(0);
   x = 1;
   std::cout << *px;  // 1?
}

compiles under GCC 4.8(并产生“预期”输出),但我怀疑它完全是 UB,因为动态对象的类型为 const intwhich remains part of the type)。但是,如果是这样,为什么编译器不阻止我违反const-correctness?

【问题讨论】:

标签: c++ c++11 constants language-lawyer const-correctness


【解决方案1】:

tl;dr:是的,这是未定义的行为。不,编译器不会诊断它。


一般来说,编译器不会(有时也不能)诊断 UB。 const-正确性违规的更明显示例实际上是 ill-formedcan be diagnosed:

#include <iostream>

int main()
{
   const int x = 0;
   x = 1;
   std::cout << x;
}

// g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
// main.cpp: In function 'int main()':
// main.cpp:6:6: error: assignment of read-only variable 'x'
//     x = 1;
//       ^

但是,除此之外,它won't stop you from performing obvious violations of const-correctness

#include <iostream>

int main()
{
    const int x = 0;
    *const_cast<int*>(&x) = 1;
    std::cout << x;
}

// Output: 1

所以,回到你的代码 sn-p,我不会对那里的编译器诊断有太多期望。

不过,您的代码确实会调用未定义的行为。让我们检查一下:

#include <iostream>

int main()
{
   int x = 0;
   const int* px = new (&x) const int(0);
   x = 1;
   std::cout << *px;  // 1?
}

会发生什么:

  1. 创建一个int,并自动存储持续时间,初始化为0
  2. 名称x 指代此对象。
  3. 使用动态存储持续时间创建 const int,重复使用 int 的存储。
  4. int 的生命周期结束1, 2
  5. x 现在指的是const int3
  6. 虽然名称 x 的类型为 int,但它现在指的是 const int,因此分配未定义4

这是一个有趣的漏洞,您可以用来“绕过”const-正确性,只要原始 int 不在只读内存中,它甚至可能不会导致崩溃。

然而,它仍然是未定义的,虽然我看不出有哪些优化可能会破坏作业和后续阅读,但你肯定会对各种意想不到的坏事持开放态度,比如你后花园的自发火山或您辛苦赚来的所有代表都被转换成英镑并存入我的银行账户(谢谢!)。


脚注 1

[C++11: 3.8/1]: [..] T 类型的对象的生命周期结束于:

  • 如果 T 是具有非平凡析构函数 (12.4) 的类类型,则析构函数调用开始,或者
  • 对象占用的存储空间被重用或释放。

脚注 2

请注意,我不必显式调用 int 对象上的“析构函数”。这主要是因为这些对象没有析构函数,但即使我选择了一个简单的类T 而不是int,我也可能不需要显式的析构函数调用:

[C++11: 3.8/4]: 程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。 对于具有非平凡析构函数的类类型的对象,在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数然而,如果没有显式调用析构函数,或者如果没有使用删除表达式 (5.3.5) 来释放存储,不应隐式调用析构函数并且任何程序取决于析构函数产生的副作用具有未定义的行为。

脚注 3

[C++11: 3.8/7]: 如果在一个对象的生命周期结束后,该对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象, > 指向原始对象的指针、指向原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用于操作新对象,如果

  • 新对象的存储正好覆盖了原对象占用的存储位置,并且
  • 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
  • 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且
  • 原始对象是T 类型的最衍生对象(1.8),而新对象是T 类型的最衍生对象(也就是说,它们不是基类子对象)。 [..]

脚注 4

[C++11: 7.1.6.1/4]: 除了可以修改任何声明为可变的类成员 (7.1.1) 之外,任何在其生命周期 (3.8) 期间修改 const 对象的尝试都会导致未定义的行为。 [..]

(以下示例与您的代码 sn-p 相似但不完全相同。)

【讨论】:

  • “我看不出有哪些优化可能会破坏分配和后续读取”。我不知道你假设编译器有多聪明,但是:逃逸分析表明 px 在初始化后没有被修改,它总是指向那个放置新的 const 对象。因此编译器可能会将*px 视为对const 的引用,并将*px 的所有使用替换为*px 的初始化值,即0。不会导致火山爆发,但编译器可能会这样做或实际上可能从最后一行的*px 加载这一事实足以证明将其设为 UB。
  • @SteveJessop:面对其他翻译单元中可能定义的operator news,它能做到吗?
  • 哎呀,我的回滚/取消回滚/重新回滚不再折叠。那好吧。 @MattPhillips:我不明白为什么我不能维护“提问者”/“回答者”抽象。为什么会“烦人”?
  • 因为就好像你在假装有两个人参与了这个线程,而实际上只有一个人,你呢?就好像你的问题被另一位专家认为很有趣,可以引起一个经过深思熟虑的回应。不是,或者任何可能的这样的答案都被你抢占了。 (在撰写本文时。)这很有趣——我赞成 Q 和 A——但都是你。我的意思是我不认为你真的想欺骗任何人,但直到我走到最后,我都有错误的印象,所以:烦人。
  • 我个人不喜欢阅读 cmets 中的元辩论。难道我们不能坚持物质并将其他东西移到它所属的地方吗?
猜你喜欢
  • 2014-05-12
  • 2020-11-10
  • 2019-05-03
  • 1970-01-01
  • 1970-01-01
  • 2011-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多