【问题标题】:Accessing owner in destructor c++在析构函数 C++ 中访问所有者
【发布时间】:2016-10-23 22:14:17
【问题描述】:

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

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

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

【问题讨论】:

  • 为什么需要从B的析构函数中访问A?
  • 请澄清您所说的“拥有一个对象 B”是什么意思?它通过智能指针拥有它?
  • 是的,A 有一个 std::unique_ptr
  • 它不是循环引用。 A拥有B,B不拥有A。
  • @OlzhasZhumabek shared_ptr 无济于事,它的析构函数仍将被调用 2 次:A destructor -> B destructor -> shared_ptr(to A) destructor -> A destructor -> B destructor -> shared_ptr destructor = 双析构函数 = 未定义行为,你不是说weak_ptr

标签: c++ smart-pointers ownership-semantics


【解决方案1】:

在 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)期间调用。 当从构造函数或析构函数直接或间接调用虚函数时,包括 在类的非静态数据成员的构造或销毁期间,以及该类的对象 call apply 是正在构造或销毁的对象(称为 x),被调用的函数是最终的覆盖器 在构造函数或析构函数的类中,而不是在派生更多的类中覆盖它。如果虚拟 函数调用使用显式类成员访问(5.2.5)并且对象表达式引用完整 x 的对象或该对象的基类子对象之一,但不是 x 或其基类子对象之一, 行为未定义。

【讨论】:

  • 在 B 的析构函数中检测我们不应该访问 A 的安全方法是什么?
  • 真的这么简单吗?显然,it is ok to use this during a destructor。所以我认为在析构函数期间调用使用this 的函数是可以的,那么为什么成员在析构函数期间不能使用指向父级的指针呢?当然,您必须小心您访问的其他成员。
【解决方案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。

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

      【解决方案4】:

      考虑一下:

      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 的构造函数和析构函数中访问,如上例所示。

      【讨论】:

      • @ChrisDrew: 已回答 [1] B 的析构函数中访问A 是否安全 其答案为否。[2] 安全方式要在 A 的析构函数中显式重置对 B 的强引用,则始终可以这样做,因为可以在析构函数中访问类的数据成员。
      • 我认为你错了。 B 的析构函数在 A 之后开始。但是 B 的析构函数在 A 的析构函数完成之前完成。
      • @ksb: 如果B 的析构函数在之后 A 开始,那么B 的析构函数如何在A 之前完成?
      • "A 拥有 B,即 B 是 A 的数据成员,因此 B 的析构函数将首先完成" 这是正确的,与我所说的相同。但是在您的回答中,您写道“如果您看到上面的示例,那么首先 a 被破坏,然后 b 被破坏。a 的析构函数不会调用 b 的析构函数,因此控件将返回到 a 的析构函数。”在内部,a 的析构函数确实调用了 b 的析构函数,并且控制权回到了 a 的析构函数。
      • 我在内部使用过这个词。编译器还添加了调用所有成员对象的基类析构函数和析构函数的代码。我认为这也是析构函数本身的一部分。如果我们手动调用~Foo(),Foo的成员对象也会被销毁。
      【解决方案5】:

      我不是语言律师,但我认为没关系。您正踏上危险的道路,也许应该重新考虑您的设计,但如果您小心的话,我认为您可以依靠members are destructed in the reverse order they were declared这一事实。

      这样就好了

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

      Live demo.

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

      Live demo.

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

      【讨论】:

        猜你喜欢
        • 2019-08-10
        • 1970-01-01
        • 2020-02-15
        • 1970-01-01
        • 2010-10-28
        • 1970-01-01
        • 2023-03-18
        • 2015-05-14
        • 2011-08-18
        相关资源
        最近更新 更多