公开类拥有的资源的正确方法是啥?
Posted
技术标签:
【中文标题】公开类拥有的资源的正确方法是啥?【英文标题】:What is the right way to expose resources owned by a class?公开类拥有的资源的正确方法是什么? 【发布时间】:2014-05-31 11:41:56 【问题描述】:假设我有一个库,它有一个 Document
类。 Document
的一个实例可以拥有多个Field
实例。 Field
有多个子类(例如IntegerField
和StringField
),甚至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&
并抛出缺少的名字?
您忘记了一个替代方案:将所有权归还给调用者的getBar
,以及将所有权归还给Foo
的releaseBar
。它要求您的 Foo
类的用户在完成后始终释放其所有权。
@Joacim - 我没有忘记,但我在问题中说我想在不转移所有权的情况下这样做。 :)
【参考方案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&
,或者如果您需要它可以为空,则返回const Field*
。两者都清楚地表明Document
保留所有权。另外,将方法本身设为const
。
您可以使用返回 Field&
或 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< Bar >
(如果 C++11)或 boost::weak_ptr
(如果不是 C++11)。这清楚地表明这是堆内存,并且不会冒对不存在内存的引用的风险(就像Bar &
一样)。它还明确了所有权。
【讨论】:
我认为 weak_ptr 承诺转换为 shared_ptr 这不在问题的范围内。 我不认为它确实明确了所有权。它说,“如果你想分享这个”。很容易意外地与另一个Document
共享Field
,然后您可能对一个Document
进行更改而影响另一个。【参考方案7】:
我会返回Bar&
(可能带有const
)。
用户需要了解,如果从地图中删除元素,则引用将失效 - 但由于单一所有权模型,无论您做什么,都会出现这种情况。
【讨论】:
@MadScienceDreams:weak_ptr
没有给出 OP 想要的单一所有权模型:任何人都可以使用弱指针来共享所有权。它还增加了一些开销,只有当用户对返回的引用/指针做了一些奇怪的事情时才需要这些开销。但你是对的,在类似情况下它可能是一个合适的选择。
对不起,由于某种原因,我认为您可以将非所有权指针指向 unique_ptrs,我的错:-P
(来自我的旧评论)这还具有公开 C++98 兼容接口的好处,该接口(如果您使用 impl idiom)允许您与非 C++11 代码进行交互.以上是关于公开类拥有的资源的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章
下载 WCF 服务公开的所有 WSDL 文件的最佳方法是啥?
在反应应用程序中保护 Firebase API 密钥以使其无法公开访问的最佳方法是啥? [复制]
由于 Databricks 不公开支持 spark-redshift lib,使用 Scala spark 从 Redshift 读取/写入 Redshift 的最佳方法是啥