通过重用的非`constst`名称修改动态分配的`const`对象是否合法?

Lig*_*ica 7 c++ const const-correctness language-lawyer c++11

考虑以下程序:

#include <iostream>

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

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

Lig*_*ica 6

tl;博士:是的,它是未定义的行为.不,编译器不会诊断它.


通常,编译器不会(有时不能)诊断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;
//       ^
Run Code Online (Sandbox Code Playgroud)

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

#include <iostream>

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

// Output: 1
Run Code Online (Sandbox Code Playgroud)

所以,回到你的代码片段,我不希望那里的编译器诊断太多.

但是,您的代码确实会调用未定义的行为.我们来看看吧:

#include <iostream>

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

这是发生的事情:

  1. 一个int与自动存储持续时间,初始化为创建0.
  2. 名称x引用此对象.
  3. const int与动态存储持续时间,再次使用所创建int的存储.
  4. 所述int的寿命结束1,2.
  5. x现在指的是const int3.
  6. 虽然名称x有类型int,但它现在指的是a const int,因此赋值是未定义的4.

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

然而,它仍然是未定义的,虽然我看不出可以执行哪些优化可能会破坏作业和随后的阅读,但你肯定会对各种意想不到的肮脏行为持开放态度,例如你后花园里的自发火山或你所有的硬-earned rep被转换成英镑并存入我的银行账户(谢谢!).


脚注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限定符),和
  • 原始对象的类型不是const限定的,如果是类类型,则不包含任何类型为const限定的非静态数据成员或引用类型,以及
  • 原始对象是类型最派生的对象(1.8),T新对象是类型最派生的对象T(也就是说,它们不是基类子对象).[..]

脚注4

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

(示例如下,与您的代码段类似,但不完全相同.)