8

Say there is an object A which owns an object B via std::unique_ptr<B>. Further B holds a raw pointer(weak) reference to A. Then the destructor of A will invoke the destructor of B, since it owns it.

What will be a safe way to access A in the destructor of B? (since we may also be in the destructor of A).

A safe way me be to explicitly reset the strong reference to B in the destructor of A, so that B is destroyed in a predictable manner, but what's the general best practice?

4

5 回答 5

3

我不是语言律师,但我认为没关系。您正在踏上危险的道路,也许应该重新考虑您的设计,但如果您小心的话,我认为您可以依赖于成员以与声明相反的顺序被破坏的事实。

所以这没关系

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

现场演示

因为then的成员A按顺序被销毁。但这不行n2bn1

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

现场演示

因为在尝试使用它n2的时候已经被破坏了。B

于 2016-06-22T08:51:00.333 回答
3

在 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 对象的指针(泛左值)转换为指向 X的直接或间接基类 B 的指针(引用) ,X 的构造及其所有直接或间接基类的构造从 B 直接或间接派生的类应该已经开始并且这些类的销毁不应该完成,否则转换会导致未定义的行为。为了形成一个指向对象 obj 的直接非静态成员(或访问其值)的指针,obj的构造应该已经开始并且它的销毁应该没有完成,否则指针值的计算(或访问成员值)会导致未定义的行为。

12.7/4

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

于 2016-06-22T07:12:38.700 回答
0

正如已经提到的,没有“安全的方式”。事实上,正如 PcAF 所指出的,当你到达's 的析构函数时,生命周期A已经结束。 我也只想指出,这实际上是一件好事!销毁对象必须有严格的顺序。 现在你应该做的是事先告诉它即将被破坏。 这很简单B

B A

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

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

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

于 2016-06-22T08:21:36.653 回答
0

考虑一下:

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's 的析构函数中访问。

“因为我们也可能在 A) 的析构函数中。” . 这是不正确的。同样,当控件离开a,只有控件进入b' 析构函数。

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

于 2016-06-22T08:26:26.623 回答
-2

如果你只看两个类 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。

于 2016-06-22T07:48:22.237 回答