通过重复使用的非“const”名称修改动态分配的“const”对象是否合法

Is it legal to modify a dynamically-allocated `const` object through a re-used non-`const` name?

本文关键字:const 动态分配 修改 对象 是否      更新时间:2023-10-16

请考虑以下程序:

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

它在 GCC 4.8 下编译(并产生"预期"输出),但我怀疑它完全是 UB,因为动态对象具有类型 const int(它仍然是类型的一部分)。但是,如果是这样,为什么编译器不阻止我违反const正确性呢?

dr:是的,这是未定义的行为。否,编译器不会对其进行诊断。


通常,编译器不会(有时不能)诊断 UB。const正确性违规的更明显的例子实际上是格式不正确的,可以诊断:

#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;
//       ^

但是,除此之外,它不会阻止您执行明显违反const正确性的行为:

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

因此,回到您的代码片段,我不会期望那里的编译器诊断方式太多。

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

#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 int 3
  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]: 程序可以通过重用对象占用的存储或使用非平凡析构函数显式调用类类型的对象的析构函数来结束任何对象的生存期。对于具有非平凡析构函数的类类型的对象,程序不需要在重用或释放对象占用的存储之前显式调用析构函数;但是,如果没有显式调用析构函数,或者未使用 delete-expression (5.3.5) 来释放存储,则不应隐式调用析构函数,并且依赖于析构函数产生的副作用的任何程序都具有未定义的行为。

脚注3

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

  • 新对象的存储完全覆盖原始对象占用的存储位置,并且
  • 新对象与原始对象的类型相同(忽略顶级 CV 限定符),并且
  • 原始对象的类型不是常量限定的
  • ,如果是类类型,则不包含任何类型为常量限定或引用类型的非静态数据成员,并且
  • 原始对象是类型 T 的最派生对象 (
  • 1.8),新对象是类型 T 的最派生对象(即,它们不是基类子对象)。[..]

脚注4

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

(以下示例与您的代码片段相似,但不完全相同。