当一个物体的死亡使另一个物体生病时,如何保持封装

时间:2012-11-24 17:31:39

标签: c++ design-patterns encapsulation

当一个对象b在内部引用并使用它不拥有的对象a时,a的死亡会使b生病。这是一个简单的例子来说明这一点:

#include <iostream>

const int my_int = 5;

class A {
  private:
    int n_;
  public:
    int n() const { return n_; }
    A(int);
};

A::A(int n__) : n_(n__) {}

class B {
  private:
    const A *ap_;
  public:
    int m() const { return 1 + ap_->n(); }
    explicit B(const A *);
};

B::B(const A *const ap__) : ap_(ap__) {}

int main()
{
    std::cout << "Will put an unnamed A on the heap.\n";
    A *const p = new A(my_int);
    std::cout << "Have put an unnamed A on the heap.\n";
    std::cout << "p->n() == " << p->n() << "\n";
    B b(p);
    std::cout << "b. m() == " << b. m() << "\n";
    std::cout << "Will delete  the unnamed A from the heap.\n";
    delete p;
    std::cout << "Have deleted the unnamed A from the heap.\n";
    std::cout << "b. m() == " << b. m() << "\n"; // error
    return 0;
}

当然,可以通过让b保留a的副本而不是指向它的指针来解决这个问题,但假设b不想保留副本(因为{ {1}}占用大量内存或其他原因)。假设a仅仅指代现有的b。当a悄然死亡时,a从未注意到。然后,当b尝试使用b时,会产生不可预测的行为。

在我的计算机上,示例的输出恰好是这样的:

a

但是,在您的计算机上,结果可能是段错误或谁知道什么。

我的例子的麻烦似乎在于,这个例子间接打破了Will put an unnamed A on the heap. Have put an unnamed A on the heap. p->n() == 5 b. m() == 6 Will delete the unnamed A from the heap. Have deleted the unnamed A from the heap. b. m() == 1 的封装,让程序员记住b的持续有效性取决于{{1}的继续存在}}。当程序员忘记时,程序会中断。因此,受骚扰的程序员在b 工作时必须记住a,即使类型A不关心类型B 。如您所知,面向对象的程序员如果能够帮助它们,则不必考虑这些琐事。

我在编程时偶尔会遇到这个问题。我今天又见过它了。有人认为,不知何故,应该存在一个优雅的设计模式来维持b的正确封装,从程序员转移到编译器,负责记住ab的依赖。存在 - 并且该模式从根本上应该包含比智能指针和完整引用计数更精细的东西。但是,也许我错了。也许这正是他们的引用计数的智能指针。无论哪种方式,我都不知道适用于问题的正确模式,也不知道修复代码的最佳方法。

如果你知道,你会告诉它吗?

Here是我在Stackoverflow上已经注意到的最接近相关的答案;但是,除了使用一两个我不理解的单词之外,无论如何,这个答案似乎都没有回答这个问题。

(我的编译器仍然不能很好地支持C ++ 11,但如果C ++ 11带来了一个专门用于解决我的问题的功能,那么当然我应该有兴趣了解它。但是,不可否认,我的问题主要涉及OO /范围基础。问题是对底层设计模式比对最新编译器的这个或那个新特性更感兴趣。)

给读者注意

一些好的答案使这个问题更加突出。在Stackoverflow上,如您所知,提问者有责任接受最佳答案,以便(当您在几个月或几年后阅读时),您将不必搜索它。

然而,这是两个答案的组合,最能回答这个问题。你应该读到两个:

  • @ MatthieuM。answer共享所有权和观察者模式;和
  • @JamesKanze的answer为什么以及何时可以选择观察者模式。

4 个答案:

答案 0 :(得分:4)

这取决于第一个对象被删除的原因。如果它是 只是因为有人认为没有人使用它,那么你可能会 能够避免智能指针的问题 - 你仍然 必须注意周期,但如果使用的原因 动态分配只是为了避免过于昂贵的副本, 对象可能只是数据,并没有任何数据 指针,所以你很安全。

当然,更常见的是对象存在的原因 删除是因为程序逻辑需要它,并推迟 通过某种智能指针删除会破坏 程序逻辑。在这种情况下,类具有指针 必须通知对象该对象已被删除,a 因此行事。在这种情况下,观察者模式是 标准解决方案您的对象b会在A注册 对象,被告知其消亡,并做任何必须做的事情 结果。如果我们采用您的B类的简单情况 是SessionA类是Connection。如果是实际的 连接丢失,Connection课程将被通知,并且 自毁。这样做(在它的析构函数中),它会 通知所有注册的对象。如果你在 服务器,Session类可能会记录问题和 自我毁灭本身 - 掉线连接终止了 会话。但是,在客户端中,Session类可能会尝试 创建一个新的Connection,并且只在中止会话 失败。

答案 1 :(得分:3)

智能解决方案是使用智能指针,如果你真的需要只保留一个对象,而不是多个副本!

C ++ 11为您提供了multiple choices可供选择:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

在你的情况下,我认为你需要第二个:std::shared_ptr

答案 2 :(得分:3)

我可以想到两个特殊的解决方案:

  • 共享所有权
  • 观察者模式

在共享所有权解决方案中,a的生命周期由所有者的数量的计数器确定,只有当没有所有者时才会a'一生终于结束了。这通常使用std::shared_ptr<A>实现。

在观察者解决方案中,当a传递给b时,a会记住b拥有对它的引用。然后,当a死亡时,它会立即通知b或留下“{token}”后面的b,以便在下一次访问尝试时得到通知。

直接通知通常通过维护当前引用者的列表并在销毁时调用每个引用来处理(因此可以删除它们的引用)。这是典型的观察者模式。

间接通知通常通过让代理对象进入a来处理。当a死亡时,通知代理(O(1)),当b尝试访问a时,它必须通过代理。自行实现这一点有很多困难,因此更好的方法是使用标准设施:std::shared_ptr这次加上std::weak_ptr

最后,这些解决方案并不相同。

  • 共享所有权意味着ab生存期间不会死亡,而观察员计划允许a先死亡
  • 直接通知让b立即对a的死亡做出反应,但同时可能会使程序更加脆弱(在a执行析构函数期间,你不应该抛出任何例外)

选择你自己的毒药:)

答案 3 :(得分:1)

您正在寻找的模式是“如果您拥有一个对象,请不要放弃对它的非拥有引用,然后销毁它。”我不认为它有更好的名字,但它真的只是很好的C ++礼仪。如果你放弃一个指向一个可以保持它的对象的指针,你需要知道该对象可能依赖指针保持有效。在这种情况下,您只需确保对象的生命周期超过非拥有对象的生命周期。

如果你正在传入一个函数,并且你可以假设该函数没有使用全局状态,那么我不会担心它 - 一旦函数返回,你应该假设它是用指针​​完成的

正如Nawaz所说,停止担心这种情况的真正方法是使用具有适当所有权语义的智能指针。如果要将所有权转移到其他对象,请使用std::unique_ptr。如果您想共享所有权,请使用std::shared_ptr。如果你想使用一个非拥有的原始指针,我不会说,但至少知道它可能导致的问题。


为了避免过早地意外删除对象,可以执行以下操作:

const std::unique_ptr<A> p(new A(my_int));
B b(p.get());
// the A object will be destroyed at the end of the current scope

这里你没有std::shared_ptr的(非常小的!)开销。我不确定我是否真的推荐它。全智能而不是半智能更为可取。 const std::unique_ptr具有与boost::scoped_ptr类似的所有权语义:它表示“我拥有这个而且没有其他人会这样做。”不同之处在于,您无法像对reset那样在此const std::unique_ptr上致电boost::scoped_ptr