在现有对象上使用新放置时,如何定义对象的内容

How are contents of an object defined when using placement new on existing object

本文关键字:对象 何定义 定义 新放      更新时间:2023-10-16

查看以下示例。C++标准是否保证object.x的值最终等于1?如果我不调用析构函数object.~Class();呢?

#include <new>
class Class
{
public:
Class() {}
~Class() {}
int x;
};
int main()
{
Class object;
object.x = 1;
object.~Class();
new (&object) Class();
object.x == ?
Class object_2;
object_2.x = 1;
new (&object_2) Class();
object_2.x == ?
return 0;
}

x等于1的对象已销毁。

你知道,因为是你毁了它。

您的新x未初始化,并且有一个未指定的值,由于内存重用,实际中可能是1。这与任何其他未初始化的值没有什么不同。


更新

由于似乎有很多人在抛出断言,下面是一些直接来自标准的事实

对于这种情况没有直接的说明,因为它遵循了管理对象是什么以及对象生存期意味着什么的一般规则。

一般来说,一旦你发现C++是一个抽象,而不是字节的直接映射,你就可以理解这里发生了什么,以及为什么没有OP所寻求的这样的保证。

首先,关于物体寿命和破坏的一些背景:

[C++14: 12.4/5]:如果析构函数不是用户提供的,并且如果:

  • 析构函数不是CCD_ 9
  • 其类的所有直接基类都有琐碎的析构函数,并且
  • 对于类类型(或其数组)的类的所有非静态数据成员,每个此类都有一个平凡的析构函数

否则,析构函数是非平凡的

[C++14: 3.8]:[..]类型为T的对象的生存期在以下情况下结束:

  • 如果T是一个具有非平凡析构函数(12.4)的类类型,则析构函数调用启动,或者
  • 对象占用的存储器被重新使用或释放

[C++14: 3.8/3]:本国际标准中赋予对象的属性仅适用于给定对象的使用寿命[..]

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

(本文的大部分内容与您的第一种情况无关,因为确实有一个显式析构函数调用:您的第二种情况违反了本段中的规则,因此具有未定义的行为;不应进一步讨论。)

[C++14: 12.4/2]:析构函数用于销毁其类类型的对象[..]

[C++14: 12.4/11]:[..]析构函数也可以显式调用。

[C++14: 12.4/15]:一旦为对象调用了析构函数,该对象就不存在了[..]

现在,一些细节。如果我们要在放置新的object.x之前检查一下呢?

[C++14: 12.7/1]:[..]对于具有非平凡析构函数的对象,在析构函数完成执行后引用该对象的任何非静态成员或基类会导致未定义的行为

哇,好吧。

如果我们在放置新的之后检查它呢?即,新对象中的CCD_ 20保持什么值?正如问题所问,它是否保证会是1?请记住,Class的构造函数不包括x:的初始化程序

[C++14: 5.3.4/17]:创建T类型对象的新表达式将该对象初始化如下:

  • 如果省略新初始值设定项,则默认初始化对象(8.5);如果不执行初始化,则对象的值不确定
  • 否则,新初始值设定项将根据8.5的初始化规则进行解释,用于直接初始化

[C++14: 8.5/16]:表单中发生的初始化

T x(a);
T x{a};

以及在new表达式(5.3.4)、static_cast表达式(5.2.9)、函数表示法类型转换(5.2.3)以及基初始化器和成员初始化器(12.6.2)中,称为直接初始化

[C++14: 8.5/17]:初始化程序的语义如下。目标类型是要初始化的对象或引用的类型,源类型是初始化项表达式的类型。如果初始值设定项不是单个(可能是带括号的)表达式,则不定义源类型[..]

  • 如果初始值设定项是(),则对象被值初始化
  • [..]

[C++14: 8.5/8]:对类型为T的对象进行值初始化意味着:

  • 如果T是一个(可能是cv限定的)类类型(第9条),没有默认构造函数(12.1),或者是用户提供或删除的默认构造函数,则对象默认初始化
  • [..]

[C++14: 8.5/7]:默认初始化T类型的对象意味着:

  • 如果T是一个(可能是cv限定的)类类型(第9条),则T的默认构造函数(12.1)被称为(如果T没有默认构造函数或重载解析(13.3)导致模糊性或在从初始化的上下文中被删除或不可访问的函数中)
  • 如果T是数组类型,则默认初始化每个元素;--否则,不执行初始化

如果程序调用常量限定类型T的对象的默认初始化,则T应为具有用户提供的默认构造函数的类类型。

[C++14: 12.6.2/8]:在非委托构造函数中,如果给定的非静态数据成员或基类不是由mem初始值设定项id指定的(包括由于构造函数没有ctor初始值设定项),并且实体不是抽象类的虚拟基类(10.4),则:

  • 如果实体是具有大括号或相等初始值设定项的非静态数据成员,则实体将按照8.5中的规定进行初始化
  • 否则,如果实体是匿名联合或变体成员(9.5),则不执行初始化
  • 否则,实体默认初始化(8.5)

[C++14: 8.5/12]:如果没有为对象指定初始值设定项,则默认初始化该对象如果不执行初始化,则具有自动或动态存储持续时间的对象的值不确定

那么,标准是否保证替换对象的x具有值1?没有。没有。

在实践中,为什么可能不是呢?原因有很多。Class的析构函数是非平凡的,因此,根据3.8,第一个对象的生存期在调用其析构函数后立即结束。

从技术上讲,编译器可以自由地将对象放置在该位置,只要在新放置生效时对象已被破坏即可。它没有理由在这里这样做,但没有什么禁止它;更重要的是,在析构函数调用和placementnew之间的一个简单的{ int x = 5; x = 42; }将更有权重用该内存。在这一点上,它没有被用来表示任何对象!

更现实地说,有些实现(例如Microsoft Visual Studio)对于调试模式程序,将可识别的位模式写入未使用的堆栈内存,以帮助诊断程序故障。没有理由认为这样的实现不会为了实现这一点而挂接到析构函数中,而且这样的实现会覆盖1值。标准中没有任何内容禁止这样做。

事实上,如果我用std::cout语句替换代码中的?行,以便实际检查值,那么在使用析构函数的情况下,我会收到关于未初始化变量和值0的警告:

main.cpp: In function 'int main()':
main.cpp:19:23: warning: 'object.Class::x' is used uninitialized in this function [-Wuninitialized]
std::cout << object.x << 'n';
^
0
1

现场演示

我不确定你还需要多少证据来证明标准不能保证这里的1值。