对象的生命周期,在哪种情况下重用存储?

时间:2017-01-16 14:22:09

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

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

at§1.8C++对象模型:

  

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

=>好的,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;

在赋值new (this) C(other)期间计算的表达式*pc1=c2是存储重用,因为pc1指向的对象具有由unsigned char数组提供的存储。

以下断言(及其前身)是否正确:

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

编辑:好的,请不要专注于“存储重用”术语,并关注“如果初始对象是在无符号字符数组提供的存储上构造的话,§3.8.8不适用”这个问题吗?

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

__begin_

然后你清楚,它将调用分配器的销毁,它将调用对象的析构函数:

 new (__begin_) value_type(data);

然后,如果你进行新的push_back,向量将不会分配新的存储:

 __begin_->~value_type();

因此根据de§3.8.8如果new (__begin_) value_type(data); 有一个ref数据成员或const数据成员,那么前面的调用将导致value_type不会指向新推送的对象。

所以我认为存储重用在$ 3.8.8中有一个特殊意义,否则,std库实现者是错误的?我查看了libstdc ++ et libc ++(GCC和Clang)。

这个例子会发生什么:

*__begin_

2 个答案:

答案 0 :(得分:7)

  

=>好的,unsigned char数组可以为其他对象提供存储,如果一个新对象占用了之前被其他对象占用的存储,则新对象会重用前一个对象的存储。

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

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

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

  

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

没有。 [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重用相同的存储。

  

编辑:好的,请不要专注于“存储重用”术语,并关注“如果初始对象是在无符号字符数组提供的存储上构造的话,§3.8.8不适用”这个问题吗?

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

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

这就是[basic.life] / 8所说的确切情况。正在创建的新对象与旧对象的类型相同。新对象完全覆盖旧存储的存储。根据{{​​1}}的性质,对象不能是任何事物的基础子对象。 vector不允许您自己粘贴vector个合格的对象。

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

即使最后一个案例也可以通过实现vector指针来满足。哦,launder new ,来自C ++ 17。 C ++ 14没有规定如何处理[basic.life] / 8不适用的类型。

答案 1 :(得分:-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;
    }
};

现在构建后不能更改值,但仅凭借访问控制:没有公共成员允许此类更改。在这种情况下,它对于类用户来说是一个不变量,但从语言语义的角度来看并不是那么多(这是有争议的......),作为一个用户,你仍然可以使用新的位置。

但是当一个成员是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]恰好是引用向量中第一个对象的左值,它不是名称。