在析构函数 c++ 中访问所有者



假设有一个对象 A 通过std::unique_ptr<B>拥有一个对象 B。进一步的 B 包含对 A 的原始指针(弱)引用。然后 A 的析构函数将调用 B 的析构函数,因为它拥有它。

在B 的析构函数中访问 A 的安全方法是什么?(因为我们也可能在 A 的析构函数中)。

一种安全的方法是在 A 的析构函数中显式重置对 B 的强引用,以便以可预测的方式销毁 B,但一般的最佳实践是什么?

我不是语言律师,但我认为没关系。你踩在危险的地面上,也许应该重新考虑你的设计,但如果你小心的话,我认为你可以依靠这样一个事实,即成员以与宣布的顺序相反的方式被破坏。

所以这没关系

#include <iostream>
struct Noisy {
int i;
~Noisy() { std::cout << "Noisy " << i << " dies!" << "n"; }
};
struct A;
struct B {
A* parent;
~B();
B(A& a) : parent(&a) {}
};
struct A {
Noisy n1 = {1};
B     b;
Noisy n2 = {2};
A() : b(*this) {}
};
B::~B() { std::cout << "B dies. parent->n1.i=" << parent->n1.i << "n"; }
int main() {
A a;
}

现场演示。

由于A的成员按顺序被破坏n2然后b然后n1.但这不行

#include <iostream>
struct Noisy {
int i;
~Noisy() { std::cout << "Noisy " << i << " dies!" << "n"; }
};
struct A;
struct B {
A* parent;
~B();
B(A& a) : parent(&a) {}
};
struct A {
Noisy n1 = {1};
B     b;
Noisy n2 = {2};
A() : b(*this) {}
};
B::~B() { std::cout << "B dies. parent->n2.i=" << parent->n2.i << "n"; }
int main() {
A a;
}

现场演示。

因为n2B尝试使用它时已经被摧毁了。

B 的析构函数中访问 A 的安全方法是什么?(因为我们也可能在 A 的析构函数中)。

没有安全的方法

3.8/1

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

— 如果 T 是具有非平凡析构函数 (12.4) 的类类型,则析构函数调用将启动 [...]

我认为在对象的生命周期结束后无法访问它很简单。

编辑:正如克里斯·德鲁(Chris Drew)在评论中所写的那样,您可以在析构函数启动后使用对象,对不起,我的错误我错过了标准中的一个重要句子:

3.8/5

在对象的生存期开始之前,但在对象将占用的存储之后 已分配,或者在对象的生存期结束后,在对象占用的存储之前 重用或释放,指向对象将位于或曾经所在的存储位置的任何指针 可以使用,但只能以有限的方式使用。对于正在建造或销毁的物体,请参见 12.7。否则 此类指针是指分配的存储 (3.7.4.2),并且使用该指针就像指针的类型为 void* 一样, 定义明确。这样的指针可以取消引用,但生成的左值只能在有限的范围内使用 方式,如下所述。在以下情况下,程序具有未定义的行为: [...]

在12.7 中,列出了在建造和破坏过程中可以做的事情,其中一些是最重要的:

12.7/3:

显式或隐式地将引用类 X对象的指针(glvalue)转换为指针(引用) 对X的直接或间接基类B,X的构造及其所有直接或间接或 直接或间接派生自B的间接基础应该已经开始,并且这些类的破坏不应该完成,否则转换会导致未定义的行为。形成指向(或 访问对象的值) 一个对象的直接非静态成员OBJ,则 OBJ 的构造应该已经开始 并且其销毁不应完成,否则指针值的计算(或访问 成员值)会导致未定义的行为。

12.7/4

成员函数,包括虚函数 (10.3),可以在构造或销毁(12.6.2)期间调用。 当从构造函数或析构函数直接或间接调用虚函数时,包括 在构造或销毁类的非静态数据成员期间,以及 调用应用是正在构造或销毁的对象(调用它X),调用的函数是最终覆盖器 在构造函数或析构函数的类中,而不是在派生更多的类中重写它。如果虚拟 函数调用使用显式类成员访问 (5.2.5),对象表达式引用完整的 x 的对象或该对象的基类子对象之一,但不是 x 或其基类子对象之一, 行为未定义。

如前所述,没有"安全的方法"。事实上,正如PcAF所指出的,当你到达B的析构函数时,A的生命周期已经结束了。
我也只想指出,这其实是一件好事!必须有一个严格的顺序来销毁物体。
现在你应该做的是事先告诉BA即将被摧毁。
它就像

void ~A( void ) {
b->detach_from_me_i_am_about_to_get_destructed( this );
}

传递this指针可能是必要的,也可能不是,具体取决于设计 obB(如果B包含许多引用,则可能需要知道要分离哪一个。如果它只包含一个,则this指针是多余的)。
只需确保适当的成员函数是私有的,以便接口只能以预期的方式使用。

备注: 这是一个简单的轻量级解决方案,如果您自己完全控制AB之间的通信,那就很好了。在任何情况下都不要将其设计为网络协议!这将需要更多的安全围栏。

考虑一下:

struct b
{
b()
{
cout << "b()" << endl;
}
~b()
{
cout << "~b()" << endl;
}
};
struct a
{
b ob;
a()
{
cout << "a()" << endl;
}
~a()
{
cout << "~a()" << endl;
}
};
int main()
{
a  oa;
}
//Output:
b()
a()
~a()
~b()

"然后 A 的析构函数将调用 B 的析构函数,因为它拥有它。对于复合对象,这不是调用析构函数的正确方法。如果你看到上面的例子,那么首先a被摧毁,然后b被摧毁。a的析构函数不会调用b的析构函数,以便控件返回到a的析构函数。

"在 B 的析构函数中访问 A 的安全方法是什么?">根据上面的例子a已经被销毁a因此无法在b的析构函数中访问。

"因为我们也可能在 A) 的析构函数中。">这是不正确的。同样,当控件超出a的析构函数时,只有控件进入b的析构函数。

析构函数是类 T 的成员函数。一旦控件脱离析构函数,就无法访问类 T。按照上面的示例,可以在类 T 的构造函数和析构函数中访问类 T 的所有数据成员。

如果只看 A 类和 B 类的关系,构造很好:

class A {
B son;
A(): B(this)  {}
};   
class B {
A* parent;
B(A* myparent): parent(myparent)  {} 
~B() {
// do not use parent->... because parent's lifetime may be over
parent = NULL;    // always safe
}
}

如果 A 和 B 的对象扩散到其他程序单元,就会出现问题。然后你应该使用std::memory中的工具,如std::shared_ptr或std:weak_ptr。

相关内容

  • 没有找到相关文章

最新更新