【问题标题】:Object lifetime, in which situation is reused the storage?对象生命周期,在什么情况下重用存储?
【发布时间】:2017-05-31 10:01:51
【问题描述】:

在 C++ ISO 标准 N4618 中(但它几乎也适用于 C++11 版本)可以阅读:

在 §1.8 C++ 对象模型:

如果在与“N unsigned char 数组”类型的另一个对象e 关联的存储中创建完整对象(5.3.4),则该数组提供存储 对于创建的对象... [注意:如果数组的该部分先前为另一个对象提供了存储,则该对象的生命周期 结束,因为它的存储被重复使用]

=> OK,unsigned char数组可以为其他对象提供存储空间,如果一个新对象占用了之前被其他对象占用的存储空间,那么新的对象会复用之前的存储空间。

在 §3.8.8 对象生命周期

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在存储位置创建一个新对象原来占用的对象,...

=> 我可以在另一个对象的存储位置构造一个对象,但是这个操作不是“存储重用”(否则为什么要写 ...在对象占用的存储被重用之前...)

作为 §3.8.8 的示例

struct C {
 int i;
 void f();
 const C& operator=( const C& );
};

const C& C::operator=( const C& other) {
  if ( this != &other ) {
    this->~C();          // lifetime of *this ends
    new (this) C(other); // new object of type C created
    f();                 // well-defined
  }
  return *this;
}

C c1;
C c2;
c1 = c2;  // well-defined
c1.f();  // well-defined; c1 refers to a new object of type C

因此,在本例中,new(this) C(other) 不会是存储重用,因为 c1 具有自动存储持续时间。

在这个例子中恰恰相反:

alignas(C) unsigned char a[sizeof(C)];
auto pc1 = new (&a) C{};
C c2;
*pc1 = c2;

在赋值*pc1=c2 期间评估的表达式new (this) C(other) 是存储重用,因为pc1 指向的对象具有由无符号字符数组提供的存储。

以下断言(以及之前的断言)是否正确:

  • 如果初始对象是在无符号字符数组提供的存储上构造的,则§3.8.8 不适用;
  • “存储重用”一词仅适用于 unsigned char 数组提供的存储。

编辑:好的,请不要关注“存储重用”术语,而是关注“如果初始对象是在 unsigned char 数组提供的存储上构建的,则第 3.8.8 节不适用”的问题?

因为如果不是这样,那么我所知道的所有 std::vector 实现都不正确。事实上,他们将分配的存储空间保存在 value_type 类型的指针中,例如称为 __begin_。 假设你在这个向量上做了一个 push_back。该对象将在分配存储开始时创建:

 new (__begin_) value_type(data);

然后你说清楚,这会调用分配器的销毁,而分配器会调用对象的析构函数:

 __begin_->~value_type();

那么如果你做一个新的 push_back,vector 就不会分配新的存储空间了:

new (__begin_) value_type(data);

因此,根据 de §3.8.8,如果 value_type 具有 ref 数据成员或 const 数据成员,那么导致 *__begin_ 的 front 调用将不会指向新的推送对象。

所以我认为存储重用在 $3.8.8 中具有特殊含义,否则,std 库实现者错了吗?我检查了 libstdc++ 和 libc++(GCC 和 Clang)。

在这个例子中会发生什么:

 #include <vector>
 struct A{
   const int i;
 };
 int main() {
   std::vector<A> v{};
   A a{};
   v.push_back(A{});
   v.clear();
   v.push_back(A{2});
   return 0;
 }

【问题讨论】:

  • "这个操作不是"存储重用"" 为什么不呢?我认为您正在尝试阅读不存在的内容。 “重用”不是一个技术术语;它只是简单的英语。如果您在某个存储中创建一个对象,那么您就是在重复使用该存储。
  • 这实际上是我在阅读 $3.8.8 之前所相信的,一个对象是在“在存储被重用之前”在另一个对象的存储上构建的??否则如何在第一个对象的存储被重用之前在另一个对象的内存位置构造一个对象??
  • (but it almost also applies to the C++11 version) 据我所知,您引用的 §1.8 部分是 C++1z 中的新内容。
  • 这就是“几乎”的原因
  • "所以在这个例子中 new(this) C(other) 不会是存储重用,因为 c1 具有自动存储持续时间。" 你是什么意思?

标签: c++ c++11 language-lawyer c++17


【解决方案1】:

=> OK,unsigned char数组可以为其他对象提供存储空间,如果一个新对象占用了之前被其他对象占用的存储空间,那么新的对象会复用之前的存储空间。

正确,但出于错误的原因。

您引用的符号是非规范文本。这就是它出现在“[note: ...]”标记中的原因。在决定标准的实际内容时,非规范性文本没有分量。因此,您不能使用该文本来证明在 unsigned char[] 中构造对象构成存储重用。

因此,如果它确实构成了存储重用,那只是因为“重用”是由简单的英语定义的,而不是因为标准有明确的规则将其定义为“存储重用”的一种情况。

我可以在另一个对象的存储位置构造一个对象,但是这个操作不是“存储重用”(否则为什么要写......在对象占用的存储被重用之前......)

没有。 [basic.life]/8 试图解释如何在对象的生命周期结束后对对象使用指针/引用/变量名称。它解释了这些指针/引用/变量名称仍然有效并且可以访问在其存储中创建的新对象的情况。

但让我们剖析一下措辞:

如果,在对象的生命周期结束后

好的,所以我们有这种情况:

auto t = new T;
t->~T(); //Lifetime has ended.

在对象占用的存储空间被重用或释放之前

以下都没有发生

delete t; //Release storage. UB due to double destructor call anyway.
new(t) T; //Reuse the storage.

在原对象占用的存储位置创建一个新对象

因此,我们这样做:

new(t) T; //Reuse the storage.

现在,这听起来像是矛盾,但事实并非如此。 “在存储被重用之前”部分旨在防止这种情况:

auto t = new T;  //Storage created, lifetime begun.
t->~T(); //Lifetime has ended; storage not released.
new(t) T; //[basic.life]/8 applies, since storage hasn't been reused yet.
new(t) T; //[basic.life]/8 does not apply, since storage was just reused.

[basic.life]/8 是说如果您在前一个对象的销毁和您尝试创建新对象之间创建了一个新对象,则该段落不适用。也就是说,如果您双重重复使用存储,则 [basic.life]/8 不适用。

但创建新对象的行为仍然是重用存储。存储重用不是一个花哨的 C++ 术语。这只是简单的英语。它的意思正是它听起来的样子:存储用于对象 A,现在您将相同的存储重用于对象 B。


编辑:好的,请不要关注“存储重用”术语,而是关注“如果初始对象是在 unsigned char 数组提供的存储上构建的,则第 3.8.8 节不适用”的问题?

但是......它确实适用

vector 存储指向第一个元素的指针。该对象被分配和构造。然后调用析构函数,但存储仍然存在。然后存储被重复使用。

这正是 [basic.life]/8 所说的情况。正在创建的新对象与旧对象的类型相同。新对象正好覆盖旧对象的存储。根据vector 的性质,这些对象不能是任何东西的基础子对象。 vector 不允许您将 const 限定在其本身上。

[basic.life]/8 的保护非常适用:新对象可以通过指向旧对象的指针/引用来访问。因此,除非您进行大量复制/移动构造函数/赋值工作以将具有const 的类型或引用成员放在vector 中,否则它将起作用。

即使是最后一种情况也可以通过实现laundering 他们的指针来满足。哦,laundernew,来自 C++17。 C++14 没有规定如何处理 [basic.life]/8 不适用的类型。

【讨论】:

  • 不! $3.8.8: ...**在重用对象占用的存储空间之前** §3.8.8 的主题是重用 ref,pointer... 在对象生命周期结束后和存储空间被重用或释放之前。这再清楚不过了。
  • @Oliv:查看我的扩展答案。
  • 好的,谢谢你我明白措辞,所以你能看看我的编辑吗?这 §3.8.8 真的很令人不安。
  • @Oliv:见我的附录。
  • 我很难相信 [basic.life]/8 体现了反对“双重重用”的规则。这似乎非常违反直觉,我看不出有任何理由要求它。
【解决方案2】:

忽略标准的文本,这是绝对没有意义的......你必须理解它的含义,这是非常清楚的:你可以假装重建的对象与它所在的对象是同一个对象重构,如果语言语义允许这样的改变:

struct T {
    int i;
    T (int i) :i(i) {}
    void set_i(int new_i) {
        new (this) T(new_i);
    }
};

这里set_i 使用一种非常愚蠢的方式来重置成员i,但请注意,可以通过其他方式(赋值)完成完全相同的行为。

考虑

class Fixed_at_construction {
    int i;
public:
    Fixed_at_construction (int i) :i(i) {}
    int get_i() {
        return i;
    }
};

现在构造后不能更改值,只能通过访问控制:没有公共成员允许这样的更改。在这种情况下,它对于类用户来说是一个不变量,但从语言语义的角度来看并没有那么多(这是有争议的......),作为用户,您仍然可以使用placement new。

但是当一个成员是 const 限定的(而不是 volatile 限定的)或者是一个引用时,C++ 语义意味着它不能被改变。该值(或引用的所指对象)在构造时是固定的,您不能使用其他语言功能来破坏该属性。这只是意味着你不能这样做:

class Constant {
    const int i;
public:
    Constant (int i) :i(i) {}
    void set_i(int new_i) { // destroys object
        new (this) T(new_i);
    }
};

这里的新展示位置本身是合法的,但与delete this 一样多。以后根本无法使用对象:旧对象被销毁,旧对象的名称仍然引用旧对象,没有特殊许可可以用来引用新对象 .调用set_i后,只能用对象的名字来引用存储:取它的地址,用void*

但在vector 的情况下,存储的对象没有命名。该类不需要存储指向对象的指针,它只需要指向存储的指针。 v[0] 恰好是一个左值,指向向量中的第一个对象,它不是名称。

【讨论】:

  • 实际上,向量也会出现问题,这就是 std::launder 的原因。问题源于现在被认为是分配器设计中的错误:方法构造应该返回指向构造对象的指针,并且该值应该存储在向量 begin 中。但是这个问题只发生在开始的对象上,因为在 begin+sizeof(value type) 根据标准没有其他对象是值得的。幸运的是,在这种情况下,编译器的行为与 C 中的一样。
  • "根据标准没有其他对象" 这就是你意识到“所有可能的对象一直存在于它们可能存在的所有地址的地方,只是undefined values" 是一个很好的规范(并且没有阻止重大优化)
  • 何时删除此规范?我想他们这样做是为了允许更多的类型别名优化,不是吗?但是 c++ 标准中有很多漏洞,以至于如果不使用未定义的行为就不可能执行系统编程。这太令人沮丧了。我认为社区应该开始开发一种新的系统语言,其中没有任何东西是“隐藏”的,甚至对象内存同步、展开或错误分配以及所有这些使 c++ 对系统程序员如此厌恶的黑魔法,并解释为什么几乎每个系统代码都是仍然在 C 中开发(有充分的理由)。
  • @curiousguy: "忽略标准的文本" 您实际上不能忽略标记为language-lawyer 的问题中的标准文本。这就是该标签的字面意义:“关于编程语言和环境的正式或权威规范的复杂性的问题。”
  • @NicolBolas C++ 规范不是公理化规范。这是一个非常模糊的意图陈述,从表面上看没有任何意义,但有时,在上下文中,知道设计限制是可以理解的。口译是一门艺术。
猜你喜欢
  • 2021-05-23
  • 1970-01-01
  • 2013-06-11
  • 2014-11-20
  • 2020-07-30
  • 2014-10-28
  • 1970-01-01
  • 1970-01-01
  • 2020-02-26
相关资源
最近更新 更多