tl;dr:是的,这是未定义的行为。不,编译器不会诊断它。
一般来说,编译器不会(有时也不能)诊断 UB。 const-正确性违规的更明显示例实际上是 ill-formed 和 can 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?
}
会发生什么:
- 创建一个
int,并自动存储持续时间,初始化为0。
- 名称
x 指代此对象。
- 使用动态存储持续时间创建
const int,重复使用 int 的存储。
-
int 的生命周期结束1, 2。
-
x 现在指的是const int3。
- 虽然名称
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 相似但不完全相同。)