【问题标题】:What is the right way to expose resources owned by a class?公开类拥有的资源的正确方法是什么?
【发布时间】:2014-05-31 11:41:56
【问题描述】:

假设我有一个库,它有一个 Document 类。 Document 的一个实例可以拥有多个Field 实例。 Field 有多个子类(例如IntegerFieldStringField),甚至API 用户也可以对其进行子类化,并将子类实例提供给Document(假设用户可以开发自定义类型的数据来存储在字段中)。

我想通过 API 公开Document 拥有的Field 实例,以便用户可以与它们交互,但不转移所有权

这样做的正确方法是什么?

我想过:

  • 暴露 const std::unique_ptr<Field>& 引用 - 这感觉很丑
  • 暴露一个普通的 Field* 指针 - 这感觉不对,因为用户可能不确定是否应该删除该实例
  • 改用std::shared_ptr - 这感觉很糟糕,因为所有权并未真正共享

例如,

class Document {
private:
    std::map<std::string, std::unique_ptr<Field> > fields;
    // ...
public:
    // ...

    // How is this done properly?
    const std::unique_ptr<Field> &field(const std::string &name) {
        return fields[name];
    }
}

期待您的建议。
(我也欢迎有关 @Fulvio 建议的替代方法的建议。)

【问题讨论】:

  • 为什么不简单地Bar&amp; 并抛出缺少的名字?
  • 您忘记了一个替代方案:将所有权归还给调用者的getBar,以及将所有权归还给FooreleaseBar。它要求您的 Foo 类的用户在完成后始终释放其所有权。
  • @Joacim - 我没有忘记,但我在问题中说我想在不转移所有权的情况下这样做。 :)

标签: c++ c++11 unique-ptr ownership


【解决方案1】:

我通常返回对数据的引用而不是对unique_ptr的引用:

const Bar &getBar(std::string name) const {
    return *bars[name];
}

如果您希望能够返回空项目,您可以返回原始指针(如果为空,则返回 nullptr)。或者更好的是你可以使用boost::optional(C++14 中的std::optional)。

如果引用的存在时间可能比所有者长(多线程环境),我在内部使用 shared_ptr 并在访问方法中返回 weak_ptr

【讨论】:

  • 你是对的。它从规范草案中被否决。但它被移到了单独的技术规范中。
  • 我希望它尽快可用。它可以提高代码质量的用例太多了!
  • @Danvil :请参阅github.com/akrzemi1/Optional 了解工作实现。
  • 在这种情况下,我看不到 optional 相对于原始指针的任何好处。 observing 原始指针基本上可以实现相同的目标。
【解决方案2】:

如果可能的话,我会尽量避免暴露Document 的内部结构,但如果失败,我将返回const Field&amp;,或者如果您需要它可以为空,则返回const Field*。两者都清楚地表明Document 保留所有权。另外,将方法本身设为const

您可以使用返回 Field&amp;Field* 的方法的非 const 版本,但如果可以的话,我会避免这样做。

【讨论】:

    【解决方案3】:

    这还没有考虑,我猜:返回一个具有相同接口的代理对象。

    代理对象的持有者拥有该代理对象的所有权。 代理持有对被引用对象的任何引用(可能很弱)。 删除对象后,您可能会引发一些错误。 代理对象没有所有权。

    也许已经提到过。我对 C++ 不是很熟悉。

    【讨论】:

    • 总的来说这是一个非常好的主意,但恐怕更适合动态类型的语言
    【解决方案4】:

    我通常不喜欢将参考资料交给内部成员。如果另一个线程修改它会发生什么?相反,如果我不能分发副本,我更喜欢更实用的方法。像这样。

    class Foo {
    private:
        std::map<std::string, std::unique_ptr<Bar> > bars;
    
        // ...
    
    public:
    
        // ...
        template<typename Callable> void withBar(const std::string& name, Callable call) const {
            //maybe a lock_guard here?
            auto iter = bars.find(name);
            if(iter != std::end(bars)) {
                call(iter->second.get());
            }
        }
    }
    

    这样,拥有的内部永远不会“离开”拥有它的类,并且所有者可以控制不变量。如果请求的条目不存在,也可以有一些细节,比如使代码成为 noop。可以这样用,

    myfoo.withBar("test", [] (const Bar* bar){
       bar->dostuff();
    });
    

    【讨论】:

    • 好吧,可以将 ref 保存在他的 Callable 中,不是吗?
    【解决方案5】:

    正如其他人从技术角度回答的那样,我想向您指出一种不同的方法并修改您的设计。这个想法是尽量尊重the Law of Demeter,并且不授予对对象子组件的访问权限。这有点难做,没有具体的例子,我无法提供很多细节,但试着想象一个由 Pages 组成的类 Book。如果我想用您当前的设计打印一页、两页或更多页的书,我可以这样做:

    auto range = ...;
    for( auto p : book.pages(range) )
      {
      p->print();
      }
    

    在遵守得墨忒耳的同时,你将拥有

    auto range = ...;
    book.print( /* possibly a range here */ );
    

    这倾向于更好的封装,因为您不依赖 book 类的内部细节,并且如果其内部结构发生变化,您无需对客户端代码执行任何操作。

    【讨论】:

    • 这是一个好主意,但是(考虑到您的示例)如果Page 类可以有多个子代并且也可以由用户提供怎么办?在这种情况下,并非所有可能的方法在设计时都是已知的,因此暴露对象本身可能是可行的。
    • 设计总是高度上下文相关的,所以你做出的每一个选择可能会也可能不会满足你的要求。我的意图是指出一种设计替代方案,但正如我所说,在不知道您的要求/约束​​/上下文的情况下,这意味着建议在其他地方寻找可能的替代解决方案。无论如何,如果您想使用仅在子类中可用的方法,您正在将您的客户端代码调整为绕过多态性的特定类型。再说一次,很难用 Foo 和 Bar 来推理,抱歉 :)
    • 我很欣赏你的想法并更新了我的问题以更好地反映我实际在做什么。我期待着你的想法:)
    • 主要依赖Field接口。如果客户端只能使用接口级别的方法,那么您可以再次为您的文档提供一堆方法,这些方法可以将调用转发(我很简单)内部字段。如果您的客户派生类添加方法,那么您只能公开字段,并且根据您的 Document 定义,您不能使用引用但需要一个指针:return fields[name].get();此外,您的返回类型是一个普通的 Field*
    • 您如何看待其他人建议的引用或weak_ptr
    【解决方案6】:

    我会返回 std::weak_ptr&lt; Bar &gt;(如果 C++11)或 boost::weak_ptr(如果不是 C++11)。这清楚地表明这是堆内存,并且不会冒对不存在内存的引用的风险(就像Bar &amp; 一样)。它还明确了所有权。

    【讨论】:

    • 我认为 weak_ptr 承诺转换为 shared_ptr 这不在问题的范围内。
    • 我不认为它确实明确了所有权。它说,“如果你想分享这个”。很容易意外地与另一个Document 共享Field,然后您可能对一个Document 进行更改而影响另一个。
    【解决方案7】:

    我会返回Bar&amp;(可能带有const)。

    用户需要了解,如果从地图中删除元素,则引用将失效 - 但由于单一所有权模型,无论您做什么,都会出现这种情况。

    【讨论】:

    • @MadScienceDreams:weak_ptr 没有给出 OP 想要的单一所有权模型:任何人都可以使用弱指针来共享所有权。它还增加了一些开销,只有当用户对返回的引用/指针做了一些奇怪的事情时才需要这些开销。但你是对的,在类似情况下它可能是一个合适的选择。
    • 对不起,由于某种原因,我认为您可以将非所有权指针指向 unique_ptrs,我的错:-P
    • (来自我的旧评论)这还具有公开 C++98 兼容接口的好处,该接口(如果您使用 impl idiom)允许您与非 C++11 代码进行交互.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-16
    • 2019-04-14
    相关资源
    最近更新 更多