1

当一个对象在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不希望保留副本(因为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,让程序员记住 的持续有效性b取决于 的持续存在a。当程序员忘记时,程序就会中断。因此,即使类型 A 本身并不关心类型 B ,受骚扰的程序员b在工作时也必须牢记。如您所知,面向对象的程序员宁愿不必记住这些琐事,如果他们可以提供帮助的话。a

我在编程时时不时地以更复杂的形式遇到这个问题。我今天又遇到了。有人认为,不知何故,应该存在一种优雅的设计模式来维护对 的适当封装,将记住 对 存在的依赖b的责任从程序员转移到编译器——并且该模式从根本上应该包含更少的东西比智能指针和成熟的引用计数更详细。然而,也许我错了。也许这正是它们引用计数的智能指针的用途。无论哪种方式,我都不知道解决问题的正确模式,也不知道修复代码的最佳方法。ba

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

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

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

读者须知

一些很好的答案为这个问题增光添彩。如您所知,在 Stackoverflow 上,提问者有责任接受最佳答案,以便(当您在数月或数年后阅读此书时)您不必搜索它。

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

  • @MatthieuM. 的回答重新共享所有权和观察者模式;和
  • @JamesKanze 的回答关于为什么以及何时可能首选观察者模式。
4

4 回答 4

4

这取决于为什么要删除第一个对象。如果只是因为有人认为没有人在使用它,那么你也许可以避免智能指针的问题——你仍然需要注意循环,但如果使用动态分配的原因仅仅是为了避免过于昂贵的副本,该对象可能只是纯数据,并且没有任何指针,因此您很安全。

当然,更多时候,对象被删除的原因是程序逻辑需要它,而通过某种智能指针延迟删除会破坏程序逻辑。在这种情况下,必须通知具有指向该对象的指针的类该对象已被删除,这是一个后果。在这种情况下,观察者模式是标准解决方案;你的对象b会在该A 对象上注册,被告知它的消亡,然后做它必须做的任何事情。如果我们采取简单的情况,您的B班级是SessionA班级是Connection。如果实际连接断开,则Connection班级将被告知,并自毁。这样做时(在其析构函数中),它会通知所有向它注册的对象。如果您在服务器上,Session该类可能会记录问题并自毁——断开的连接会终止会话。但是,在客户端中,Session该类可能会尝试创建一个新的Connection,并且只有在失败时才中止会话。

于 2012-11-24T17:53:38.680 回答
3

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

C++11 为您提供多种选择

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

就您而言,我认为您需要第二个:std::shared_ptr.

于 2012-11-24T17:35:59.890 回答
3

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

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

在共享所有权的解决方案中, 的生命周期a是由所有者数量的计数器决定的,只有在没有所有者的情况下a, 的生命周期才会结束。这通常使用std::shared_ptr<A>.

在观察者解决方案中,当a被传递给时ba记住b持有对它的引用。然后,当a死亡时,它要么立即通知,要么b留下一个“令牌”,b以便在下一次访问尝试时得到通知。

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

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

最后,这些解决方案是不等价的。

  • 共享所有权意味着活着时a不能死,b而观察者计划允许a先死
  • 直接通知可以b立即对a' 的死亡做出反应,但同时可能会使程序更加脆弱(在a' 的析构函数执行期间,您不应抛出任何异常)

选择你自己的毒药:)

于 2012-11-24T17:49:41.687 回答
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_ptra 具有相似的所有权语义boost::scoped_ptr:它表示“我拥有这个,其他人永远不会拥有”。不同之处在于您将无法像reset在.const std::unique_ptrboost::scoped_ptr

于 2012-11-24T17:47:47.393 回答