【问题标题】:Does an object created with placement new have dynamic storage duration?使用placement new 创建的对象是否具有动态存储持续时间?
【发布时间】:2016-05-25 04:58:52
【问题描述】:

(5.3.4)

新表达式:

  • ::opt_new new-placement_opt new-type-id new-initializeropt

  • ::opt_new new-placement_opt ( type-id ) new-initializeropt

由 new-expression 创建的实体具有动态存储期限 (3.7.4)。 [注意:这种实体的生命周期不一定 仅限于创建它的范围。 ——尾注]

我认为以下有 1 个具有 自动 存储持续时间的主要对象 (local_object) 和 3 个具有 动态 存储持续时间的虚拟类。

struct dummy
{
    int a;
};

char local_object[256];
dummy * a = new(&local_object) dummy;
dummy * b = new(&local_object +100) dummy;
dummy * c = new(&local_object +200) dummy;

用户@M.M.认为只有一个对象(local_object),其余的只是指针。这是正确的吗?

(3.7)

动态存储持续时间与使用 operator new

创建的对象相关联

【问题讨论】:

  • 像这样使用内存与您刚刚使用普通旧new 给您的内存完全相同。唯一的区别是你知道它在哪里。
  • @5gon12eder 问题是*a 是动态存储时长还是自动存储时长
  • 一个相关的问题是new(&local_object)是否被认为是获取存储
  • 为什么这是一个问题? newnew。放置 new 不会使放置的对象有任何不同;它们仍然需要使用它们的析构函数来销毁。
  • @5gon12eder 好吧,这就是这个问题的意义所在,新位置是否会使对象更改为具有动态存储持续时间? (问题中提供的引号似乎表明确实如此)

标签: c++ language-lawyer


【解决方案1】:

在我看来,标准(如 OP 中所引用的)只能解释为它读取的内容,即 operator new 创建动态存储持续时间的对象,即使为自动持续时间的对象获取底层内存。

第 3.8 节 [basic.life] 第 8 段中的标准预测了这种精确场景,并参考了以下未定义行为的示例:

class T { };
struct B {
    ~B();
};
void h() {
    B b;
    new (&b) T;
}

段落内容如下:

如果程序以静态 (3.7.1)、线程 (3.7.2) 或自动 (3.7.3) 存储持续时间结束 T 类型对象的生命周期,并且如果 T 具有非平凡的析构函数,则当隐式析构函数调用发生时,程序必须确保原始类型的对象占据相同的存储位置;否则程序的行为是不确定的。

在示例中,程序通过重用其存储空间“结束了对象 b 的生命周期”,如同一部分的第 4 段所提供的那样:(已添加重点)。

程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。

在示例代码中,b 的析构函数未被调用,但这是可以接受的,因为第 4 段明确允许不调用非平凡析构函数:

对于具有非平凡析构函数的类类型的对象,在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数;

只要程序准备好承受不调用析构函数的后果。

但是回到第 8 段,b 的生命周期已经结束,并且存储已被重用于创建 T 类型的对象。该对象具有动态存储持续时间,这意味着它的析构函数不会被隐式调用。如上所述,也不必显式调用析构函数,只要程序不需要析构函数可能执行的任何副作用。

尽管b 的生命周期已经结束,但b 具有自动存储持续时间这一事实意味着当控制流离开其范围时将隐式调用其析构函数。根据 §3.8 的第 6 段,对生命周期已结束的对象调用析构函数是禁止使用生命周期已结束的对象的特定情况,它禁止调用生命周期已结束的对象的非静态成员函数已结束,但其存储尚未被重用或释放。

因此,示例程序的行为未定义。

但是本节的第 7 段为程序提供了一种机制,通过在同一位置重新创建相同类型的不同对象来避免未定义行为:

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

(7.1) — 新对象的存储正好覆盖了原始对象占用的存储位置,并且

(7.2) — 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且

(7.3) — 原始对象的类型不是 const 限定的,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且

(7.4) — 原始对象是类型 T 的最衍生对象 (1.8),而新对象是类型 T 的最衍生对象(也就是说,它们不是基类子对象)。

因此,在我的解释中,以下 sn-p 将定义行为:

class T { };
struct B {
    ~B();
};
void h() {
    B b;
    new (&b) T;
    new (&b) B; /* recreate a B so that it can be destructed */
}

简而言之,该标准考虑了可以使用分配给自动存储持续时间的对象的内存来创建动态存储持续时间的对象的可能性,并为执行此操作的明确定义的程序提供了一组限制和要求, 从而避免对一个生命周期已通过重用其存储而结束的对象执行隐式析构函数的后果。

【讨论】:

  • 所以,我们有 3.8\4: " 析构函数不应被隐式调用 " - 并且,3.8\8: " 当隐式析构函数调用发生时”。什么?
  • 哦,看来您必须通过显式调用其析构函数来结束 b 的生命周期,而不仅仅是重用其存储空间;否则,3.8\4 会有未定义的行为,不是吗?
  • @eugene: 在原始对象上没有调用隐式析构函数,因为它的存储被重用了。 (合乎逻辑,因为不容易知道存储是否被重用。)但是会调用隐式析构函数,因此此时需要另一个对象
  • 我喜欢 CXX 的作者 :)
  • @eugene:关于您的第二条评论,3.8/4 明确表示您不需要调用析构函数,除非它执行您所依赖的副作用。您如何将其解释为需要显式调用?关于您的第三条评论,也许language-lawyer 不适合您:-)
猜你喜欢
  • 1970-01-01
  • 2019-08-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-28
  • 1970-01-01
  • 2018-02-20
  • 2021-11-08
相关资源
最近更新 更多