如何处理指针成员的不同所有权策略?

Posted

技术标签:

【中文标题】如何处理指针成员的不同所有权策略?【英文标题】:How to deal with different ownership strategies for a pointer member? 【发布时间】:2011-03-22 08:25:33 【问题描述】:

考虑以下类结构:

class Filter

    virtual void filter() = 0;
    virtual ~Filter()  
;

class FilterChain : public Filter

    FilterChain(collection<Filter*> filters)
    
         // copies "filters" to some internal list
         // (the pointers are copied, not the filters themselves)
    

    ~FilterChain()
    
         // What do I do here?
    

    void filter()
    
         // execute filters in sequence
    
;

我在库中公开这个类,所以我无法控制它的使用方式。

我目前有一些关于Filter 对象所有权的设计问题FilterChain 持有指向的指针。更具体地说,这里是FilterChain 的两种可能的使用场景:

场景 A:我的库中的一些函数正在构建一个(可能很复杂)过滤器链,根据需要分配内存,并返回一个新分配的 FilterChain 对象。例如,其中一个函数从一个文件构造一个过滤器链,可以描述任意复杂的过滤器(包括过滤器链的过滤器链等)。工作完成后,函数的用户负责对象销毁。 场景 B:用户可以访问一堆Filter 对象,并希望以特定方式将它们组合到过滤器链中。用户构造FilterChain 对象供自己使用,然后在完成后销毁它们。当引用它们的 FilterChain 被销毁时,Filter 对象不得被销毁。

现在,在FilterChain 对象中管理所有权的两种最简单方法是:

FilterChain 拥有 Filter 对象。这意味着FilterChain 引用的对象在FilterChain 的析构函数中被销毁。这与场景 B 不兼容。 FilterChain 拥有Filter 对象。这意味着FilterChain 的析构函数什么都不做。现在方案 A 存在问题,因为用户必须知道所涉及的所有 Filter 对象的内部结构才能将它们全部销毁而不会丢失任何一个,因为父 FilterChain 自己不会这样做。这只是糟糕的设计,并要求内存泄漏。

因此,我需要一些更复杂的东西。我的第一个猜测是设计一个带有可设置布尔标志的智能指针,指示智能指针是否拥有该对象。然后,FilterChain 将不采用指向 Filter 对象的指针集合,而是采用指向 Filter 对象的智能指针集合。当FilterChain 的析构函数被调用时,它会破坏智能指针。然后,智能指针本身的析构函数将销毁指向的对象(Filter 对象)当且仅当设置了指示所有权的布尔标志。

我觉得这个问题在 C++ 中很常见,但是我在网上搜索流行的解决方案或聪明的设计模式并不是很成功。事实上,auto_ptr 在这里并没有真正的帮助,shared_ptr 似乎有点矫枉过正。那么,我的解决方案是不是一个好主意?

【问题讨论】:

为什么共享指针是多余的?在立即实施和维护方面比自己动手要容易得多(每个人都知道它们是什么)。开销很小。 基类析构函数必须是虚拟的。 @Patrick:对我来说感觉有点矫枉过正,因为 shared_ptr 是一个引用计数器,我只需要一个布尔标志。 @Neil Butterworth:是的,当然。为了简洁起见,我省略了它。 但是编写和测试一个完整的智能指针类不是比使用一个使用 int 而不是 bool 的预测试类更过分吗?也有理由认为 std::shared_ptr 实现会比我第一次尝试智能指针类更好(代码大小、速度等)...... 【参考方案1】:

这里的智能指针并不过分:显然你有一个设计问题,需要仔细考虑对象的生命周期和所有权。如果您希望能够在运行时重新修补过滤器图中的过滤器,或者能够创建复合 FilterChain 对象,则尤其如此。

使用shared_ptr 将一举解决大部分问题,并使您的设计更加简单。我认为这里唯一潜在的问题是您的过滤器是否恰好包含循环。如果您有某种反馈循环,我可以看到这可能会发生。在这种情况下,我建议将所有Filter 对象归一个类所有,然后FilterChain 将存储指向Filter 对象的弱指针。

我敢打赌,过滤阶段的执行时间将远远超过取消引用智能指针的额外开销。 shared_ptr 的设计非常轻巧。

【讨论】:

似乎这里的每个人都认为这是正确的做法。好吧,那我们去shared_ptr吧。 我曾经在谈到智能指针时有一点心理障碍,但最近开始使用 shared_ptr 我发现它让生活变得如此更容易。【参考方案2】:

过滤器是否如此之大,以至于您在创建FilterChain 时不能简单地对每个过滤器进行深层复制?如果您能够做到这一点,那么您的所有问题都会消失:FilterChain 总是会自行清理。

如果由于内存问题这不是一个选项,那么使用shared_ptr 似乎是最有意义的。调用者必须负责为它关心的每个对象保留一个shared_ptr,然后FilterChain 将知道是否删除特定的过滤器,当它是deleted。

编辑:正如 Neil 所说,Filter 需要一个虚拟析构函数。

【讨论】:

过滤器对象是不可复制的,不是因为内存问题,而是因为它会导致与状态信息重复相关的问题(而且,它在语义上是可疑的)。 @e-t172 那么将过滤器状态存储在shared_ptr 中以便您可以复制Filter s 呢? FilterChain中使用shared_ptr有什么区别? @e-t172 它只是改变了引用计数发生的位置。如果您允许Filter 对象具有引用计数状态,那么您可以随意复制它们,完全无需客户端创建shared_ptr 到过滤器:他们只需传入集合并理解他们仍然拥有集合中的所有过滤器,并且Filtercontainer 将在完成后正确清理其所有过滤器。【参考方案3】:

FilterChain 应该有一个单独的 DeleteAll() 方法,它遍历集合和 deletes 过滤器。它在场景 A 中调用,而在场景 B 中被调用。这确实需要 FilterChain 用户的一些智能,但只需要记住 @987654324 @并反对他们new'd。 (他们应该能够处理,否则他们应该得到内存泄漏)

【讨论】:

他们不应该记得在任何地方删除任何内容。使用容器、智能指针等。保持异常安全、干净,从不显式管理实用程序类之外的东西。【参考方案4】:

我会选择不拥有 Filter 对象的 FilterChain。然后,在您的库中,当您需要从文件中加载 FilterChain 时,您将拥有另一个 Loader 对象,该对象负责 Filter 对象的生命周期。因此,FilterChain 将始终适用于库加载的链和用户创建的链。

【讨论】:

这意味着将可能复杂的过滤器结构复制到 Loader 对象中,仅用于管理所有权。与其他解决方案相比,这过于复杂。 事实上,您提出的只是我最初问题中的第二个“解决方案”,除了您将管理过滤器对象的逻辑从库外部移动到内部。确实,对于库的用户来说,代码更简单,但仅仅因为过度的复杂性隐藏在库中并不意味着它是好的设计(这是必要的,但还不够)。 我不明白为什么需要在加载程序中复制过滤器结构。加载器只需要管理过滤器的生命周期。图书馆的用户仍然需要管理他们自己的过滤器的生命周期。过滤器应该(反)序列化自己。每当我看到这样的代码时,都是因为在 Filter 类的设计中没有足够的考虑,现在你付出了代价。 “我不明白为什么你需要在加载器中复制过滤器结构。加载器只需要管理过滤器的生命周期。”我同意。问题是,一个过滤器可以是一个简单的过滤器,也可以是一个过滤器链,或者一个链链等等,而我们刚刚回到了最初的问题。

以上是关于如何处理指针成员的不同所有权策略?的主要内容,如果未能解决你的问题,请参考以下文章

GTK+ 如何处理指针?

字符串类型数组的指针运算,C++如何处理这个?

GRPC 如何处理出现多次的指针?

您如何处理排序、分页和过滤的参数?

如何处理具有完全不同构建系统的第三方库?

如何处理 iPhone 4s、5 和 6 的屏幕尺寸,并使所有内容在所有设备上看起来都一样