我应该使用哪种智能指针?

3
我是一个帮助翻译的助手。下面是您需要翻译的内容:

我正在使用由几种不同类型(属性、父级、子级等)组成的模型。每种类型都与一组来自C API的函数相关联。例如:

Type "Properties":
  char* getName(PropertiesHandle);
  char* getDescription(PropertiesHandle);

Type "Parent"
  PropertiesHandle getProperties(ParentHandle);
  ChildHanlde getFirstChild(ParentHandle);

Type "Child"
  PropertiesHandle getProperties(ChildHandle);
  ParentHanlde getParent(ChildHandle);
  ChildHandle getNextChild(ChildHandle);

我已经为这个C API库创建了一组C++接口,如下所示:

class IProperties
{
public:
  virtual std::string getName() = 0;
  virtual std::string getDescription() = 0;
};

class IParent
{
public:
  virtual std::shared_ptr<IProperties> getProperties() = 0;
  virtual std::shared_ptr<IChild> getFirstChild() = 0;
};

class IChild
{
public:
  virtual std::shared_ptr<IProperties> getProperties() = 0;
  virtual std::shared_ptr<IParent> getParent() = 0;
  virtual std::shared_ptr<IChild> getNextChild() = 0;
};

我将通过Properties、Parent和Child这些类来实现这些接口。因此,Child实例将通过其构造函数获取其特定的ChildHandle,并且其getParent函数将会像这样:

std::shared_ptr<IParent> getParent()
{
    // get the parent handle and wrap it in a Parent object
    return std::shared_ptr<IParent>(new Parent(_c_api->getParent(_handle)));
}

你认为我在这里返回一个shared_ptr是否合理?我不能使用std::unique_ptr,因为Google Mock要求模拟方法的参数和返回值可复制。我通过Google Mock在我的测试中模拟这些接口。
我也在考虑未来可能会进行的优化,这可能会导致循环引用的可能性。如果使用缓存来避免对C API的多次调用(例如,不需要子对象多次建立父对象),再加上Child构造函数需要其Parent,则可能会导致循环引用。这将需要使用weak_ptr来更改接口和大量代码...

1
这看起来很合理。从上面的代码来看,您的对象似乎是基础对象模型的外观,并且 getParent() 正在交接新的外观实例。我唯一担心的是 Parent 相对于底层对象的生命周期。理想情况下,底层对象也应该被引用计数,并且创建外观会保留它。否则,底层对象的生命周期可能比外观还要短。 - marko
这个链接应该很有趣,或者是一个重复的问题。 - Xeo
4
如果我理解正确,你反对使用unique_ptr的唯一理由是Google Mock无法与其配合使用。这是一个糟糕的论据。测试框架不应该限制你设计的基本方面(尽管模拟总会在某种程度上做到这一点)。 - Konrad Rudolph
3个回答

2
关键问题是:返回指针的语义是什么?
  • if the returned parent/child/properties object has a lifetime independent of the returning (presumably, in some sense, owning) object, it's reasonable to return shared_ptr: this indicates that the caller and callee have equal rights to decide the object's lifetime

    std::shared_ptr<IChild> child = parent->getFirstChild();
    // now I can keep child around ... if parent is destroyed, one
    // orphaned subtree is magically kept around. Is this desirable?
    
  • if the returned object has a lifetime dependent on the callee's own lifetime, then:

    • shared_ptr will wrongly suggest it's meaningful for the caller to extend the returned object's lifetime beyond that of the callee
    • unique_ptr will wrongly suggest transfer of ownership
    • raw pointer doesn't explicitly make any misleading promises, but doesn't give any hint about correct use either

因此,如果调用者只是获取您对象的内部状态的工作引用,而没有转移所有权或扩展对象生命周期,建议不使用任何智能指针。

考虑只返回一个引用。


不,你不应该使用weak_ptr来打破循环引用。在受管理的资源中存在循环引用是没有意义的。你怎么可能同时拥有一支笔并被这支笔所拥有呢?一个子对象永远不应该拥有它的父对象。此外,请查看我的评论。 - Xeo
那么 getFirstChildgetParent 都返回 shared_ptr,但是它们内部都没有使用它? - Useless
getFirstChild 不应该返回一个 拥有 指针,而应该只返回一个引用。我认为父对象共享或甚至放弃其子对象的所有权是没有意义的。 - Xeo
我同意在这里shared_ptr的所有权语义本来就是错误的 - 为了清晰起见,我将删除weak_ptr的注释。 - Useless
如果调用者只是获取您对象的内部状态的工作引用,智能指针并不适用。它们明确用于所有权语义。除此之外,答案现在看起来很好,+1。 - Xeo

2

返回一个 shared_ptr 并没有什么问题,但我会试着说服你这可能不是最佳选择。

使用智能指针可以获得安全性的优势,但你的 API 用户将失去使用最适合他们需求的智能指针类型的灵活性,而必须始终使用 shared_ptr

当然,这也取决于你更看重安全性还是灵活性。但我个人认为,返回一个裸指针并允许用户使用他想要的智能指针是更好的选择。当然,如果由于某种原因必须使用 shared_ptr,那就使用它。


8
请勿裸指针。std::shared_ptr可以从unique_ptr&&构造,所以在我看来最好的选择是返回unique_ptr。如果您想将返回值存储在shared_ptr中,那么没有问题,您仍然可以这样做。 - mfontanini
1
不能从unique_ptr&&构造的非标准智能指针肯定也不能从shared_ptr构造,所以你会面临相同的情况。 - mfontanini
5
你总是可以使用 .release() 方法释放 std::unique_ptr,这使得你的参数现在完全无效;对于需要进行管理的资源,没有理由不返回一个 std::unique_ptr - Xeo
std::shared_ptr<int> a = std::unique_ptr<int>(new int(15));。就这样。 - mfontanini
2
@Baz: std::shared_ptr<T> sp(std::move(up)); - Xeo
显示剩余4条评论

0

shared_ptr 是不错的选择,但它对终端用户有一些限制,例如需要 C++11 支持。使用裸指针或允许用户定制智能指针的特性可能会为终端用户提供更多的灵活性。

无论使用哪种指针,我建议在实现中引入的语义要小心。当前的实现方式是,每次访问器调用都会实例化一个新的包装器,这样等价性检查将失败。请考虑以下代码:

auto child = parent->getFirstChild();
if ( parent == child->getParent() ) // Will be false, as they point to different
                                    // instantiations of Parent.
  ...

if ( child->getParent() == child->getParent() ) // False for the same reason.
  ...

auto sibling = child->getNextChild();
if ( parent == sibling->getParent() ) // Also false for the same reason.
  ... 

此外,当使用std::shared_ptr时,考虑使用std::make_shared来减少分配时发生的一些开销可能会很值得。

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