对象生存期,在这种情况下重用存储

Object lifetime, in which situation is reused the storage?

本文关键字:存储 这种情况下 生存期 对象      更新时间:2023-10-16

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

在 §1.8 C++对象模型:

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

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

在 §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指向的对象具有由无符号 char 数组提供的存储。

以下断言(和前面的断言)是否正确:

  • §3.8.8 不适用于初始对象是在无符号 char 数组提供的存储上构造的;
  • 术语"重用存储"仅适用于无符号 char 数组提供的存储。

编辑:好的,请不要关注"存储重用"术语,而专注于"如果初始对象是在无符号字符数组提供的存储上构造的,§3.8.8 不适用"?

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

new (__begin_) value_type(data);

然后你做一个明确的,这将调用分配器的销毁,这将调用对象的析构函数:

__begin_->~value_type();

然后,如果您创建一个新push_back,矢量将不会分配新的存储:

new (__begin_) value_type(data);

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

所以我认为存储重用在 3.8.8 美元中具有特殊意义,否则,std 库实现者是错误的?我已经检查了libstdc++ et 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;
}

=> 好的,无符号字符数组可以为其他对象提供存储,如果一个新对象占用了以前被另一个对象占用的存储,则新对象将重用前一个对象的存储。

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

您引用的符号是非规范性文本。这就是为什么它出现在"[注意:...]"标记中的原因。在决定标准实际内容时,非规范性文本没有权重。因此,不能使用该文本来证明在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 重用相同的存储。


编辑:好的,请不要关注"存储重用"术语,而专注于"如果初始对象是在无符号字符数组提供的存储上构造的,§3.8.8 不适用"?

但。。。它确实适用

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

这正是[basic.life]/8所谈论的情况。正在创建的新对象的类型与旧对象的类型相同。新对象完全覆盖旧对象的存储。根据vector的性质,这些对象不能是任何事物的基本子对象。vector不允许你const限定对象本身。

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

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

无视标准的文本,这绝对是胡说八道......你必须理解它的含义,这是水晶般清楚的:你可以假装一个重建的对象与重建它的对象是同一个对象,如果语言语义允许这样的改变:

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;
}
};

现在,该值在构造后无法更改,而只能通过访问控制进行更改:没有公共成员允许此类更改。在这种情况下,对于类用户来说,它是一个不变的,但从语言语义的角度来看(这是有争议的......),作为用户,你仍然可以使用放置 new。

但是,当一个成员是常量限定的(而不是易失限定的)或是一个引用时,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]恰好是一个左值,指的是向量中的第一个对象,但它不是一个名称。