如何正确地公开类拥有的资源?

20

假设我有一个库,其中包含一个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&并在缺少名称时抛出异常呢? - galop1n
你忘记了一种替代方案:一个getBar方法将所有权交给调用者,以及一个releaseBar方法将所有权归还给Foo。这需要Foo类的用户在完成后始终释放其所有权。 - Some programmer dude
1
@Joacim - 我没有忘记,但我在问题中说过我想在不转移所有权的情况下完成这个。 :) - Venemo
7个回答

13

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

用户需要了解,如果从映射中删除元素,则引用将失效 - 但无论如何都会出现这种情况,因为采用了单所有权模型。


@MadScienceDreams:weak_ptr不能提供OP想要的单一所有权模型:任何人都可以使用弱指针来共享所有权。它还会增加一些开销,只有在用户对返回的引用/指针做出奇怪的操作时才是必要的。但你说得对,在类似的情况下它可能是一个合适的选择。 - Mike Seymour
抱歉,由于某些原因我认为你可以创建指向unique_ptr的非所有权指针,我的错 :-P - IdeaHat
2
这种方式的好处还在于它同时提供了一个兼容C++98的接口,如果你使用实现习惯用法,那么就可以与非C++11代码进行交互了。 - IdeaHat

6
作为其他人从技术角度回答了您的问题,我想提供一个不同的方法来修改您的设计。这个想法是尝试尊重迪米特法则,并且不要授予对对象子组件的访问权限。这比较难做到,如果没有具体的示例,我无法提供更多细节,但请想象一个由页面组成的书籍类。如果我想打印书中的一页、两页或更多页,在您当前的设计下,我可以执行以下操作:
auto range = ...;
for( auto p : book.pages(range) )
  {
  p->print();
  }

在遵守迪米特法则的同时,你将会拥有:
auto range = ...;
book.print( /* possibly a range here */ );

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


设计总是高度上下文相关的,因此您做出的每个选择都可能或可能不满足您的要求。我的意图是指出一种设计替代方案,但正如我所说,如果不知道您的要求/限制/上下文,这只是一个建议,让您寻找可能的替代解决方案。无论如何,如果您想使用仅在子类中可用的方法,则会将客户端代码绑定到特定类型,从而绕过多态性。再次,很难用Foo和Bar这样的术语进行推理,抱歉 :) - Fulvio Esposito
感谢您的想法并更新了我的问题,以更好地反映我实际正在做的事情。期待您的想法 :) - Venemo
大多数情况下,它取决于Field接口。如果客户端只能使用接口级别的方法,那么您只需提供一堆方法来转发(我将其简化)调用内部字段的文档即可。如果您的客户端派生类添加方法,那么您只能公开字段,并且根据您的文档定义,您无法使用引用而需要指针:返回fields[name].get(); 此外,您的返回类型是纯Field*。 - Fulvio Esposito
谢谢您的想法! - Venemo
经过深思熟虑,我认为这是解决手头问题的最佳方式。谢谢。 - Venemo
显示剩余3条评论

4

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

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

如果你想返回空项目,你可以返回原始指针(在空的情况下使用nullptr)。或者更好的方法是使用boost::optional (std::optional 在C++14中可用)。
如果存在引用比宿主对象生存周期更长的可能性(多线程环境),我会在内部使用shared_ptr 并在访问方法中返回weak_ptr

你说得对。它已经从规范草案中被投票淘汰了。但它被移动到了单独的技术规范中。 - Johny
2
我希望它能尽快提供。有很多用例可以提高代码质量! - Danvil
@Danvil:请查看 https://github.com/akrzemi1/Optional/ 获取一个可用的实现。 - ildjarn
在这种情况下,我看不出使用optional相比原始指针有任何好处。一个观察原始指针基本上可以达到同样的目的。 - Avidan Borisov

1

我通常不喜欢将内部成员的引用传递出去。如果另一个线程修改了它会发生什么?相反,如果我不能交出一份副本,我更喜欢采用更加函数式的方法。像这样:

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());
        }
    }
}

这样,拥有的内部对象永远不会“离开”拥有它的类,拥有者可以控制不变量。还可以实现一些小巧的功能,比如如果请求的条目不存在,则使代码无效。可以像这样使用它:
myfoo.withBar("test", [] (const Bar* bar){
   bar->dostuff();
});

1
那么,一个人可以将引用保存在他的Callable中,不是吗? - RiaD

1
我会返回一个 std::weak_ptr< Bar >(如果是C++11)或 boost::weak_ptr(如果不是C++11)。这样可以明确表明这是堆内存,不会出现引用不存在的内存(像 Bar & 一样)。它还可以明确所有权。

我认为weak_ptr承诺转换为shared_ptr,这超出了问题的范围。 - Jon Kalb
我认为它并没有明确地表达所有权。它只是说:“如果您想共享,请分享”。很容易意外地与另一个“文档”共享“字段”,然后您可能会发现对一个“文档”的更改会影响到另一个“文档”。 - Chris Drew

1
如果可能的话,我会尽量避免暴露 Document 的内部,但如果无法做到,我要么返回一个 const Field&,要么返回一个可为空的 const Field*。两者都清楚地显示了 Document 保留所有权。此外,将方法本身设为 const
你可以有一个非 const 版本的方法,返回一个 Field&Field*,但如果可以的话,我会避免这样做。

0

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

代理对象的持有者拥有该代理对象的所有权。 代理对象保存对被引用对象的引用(可能是弱引用)。 当对象被删除时,可以触发一些错误。 代理对象不具备所有权。

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


总的来说,这是一个非常好的想法,但我担心它更适合动态类型语言。 - lethal-guitar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接