如何在优秀的C ++ API中正确使用shared_ptr

时间:2014-06-22 21:14:55

标签: c++ c++11

我目前正在尝试了解如何在C ++ API中正确使用C ++ 11的shared_ptr功能。我需要它的主要区域是容器类(例如场景图中的节点,例如可能包含子节点列表和父节点的引用以及类似的东西)。创建节点的副本不是一个选项,使用引用或指针是痛苦的,因为没有人真正知道谁负责破坏节点(当有人破坏仍被其他节点引用的节点时,程序将崩溃)。

所以我认为在这里使用shared_ptr可能是一个好主意。让我们看一下下面的简化示例(它演示了一个必须连接到父节点的子节点):

#include <memory>
#include <iostream>

using namespace std;

class Parent {};

class Child {
    private:
        shared_ptr<Parent> parent;
    public:
        Child(const shared_ptr<Parent>& parent) : parent(parent) {}
        Parent& getParent() { return *parent.get(); }
};

int main() {
    // Create parent
    shared_ptr<Parent> parent(new Parent());

    // Create child for the parent
    Child child(parent);

    // Some other code may need to get the parent from the child again like this:
    Parent& p = child.getParent();

    ...        

    return 0;
}

此API强制用户使用shared_ptr创建子级与父级之间的实际连接。但在其他方法中,我想要一个更简单的API,这就是getParent()方法返回对父级而不是shared_ptr的引用的原因。

我的第一个问题是:这是shared_ptr的正确用法吗?还是有改进的余地?

我的第二个问题是:我如何正确地对空指针作出反应?因为getParent方法返回引用,用户可能认为它永远不会返回NULL。但这是错误的,因为当有人传递包含指向构造函数的空指针的共享指针时,它将返回NULL。其实我不想要空指针。必须始终设置父级。我该如何妥善处理?通过手动检查构造函数中的共享指针并在包含NULL时抛出异常?或者,还有更好的方法?也许是某种不可空的共享指针?

3 个答案:

答案 0 :(得分:8)

在您描述的目的中使用共享指针是合理的,并且在C ++ 11库中越来越常见。

需要注意几点:

  • 在API上,将shared_ptr作为参数强制调用者构造shared_ptr。这绝对是一个很好的举动,其中有一个指针的所有权转移。如果函数仅使用shared_ptr,则可以接受对象的引用或shared_ptr

  • 您正在使用shared_ptr<Parent>来保持父对象的后向引用,而在另一个方向上使用一个。这将创建一个保留周期,从而导致永远不会被删除的对象。通常,在从上到下引用时使用shared_ptr,在引用时使用weak_ptr。特别注意委托/回调/观察者对象 - 这些对象几乎总是需要weak_ptr给被调用者。如果lambdas异步执行,你还需要注意它们。一种常见的模式是捕获weak_ptr

  • 通过引用而不是值传递共享指针是一个带有参数for和against的样式点。很明显,当通过引用传递时,您没有通过所有权(例如,增加对象的引用计数)。另一方面,你也没有花费开销。以这种方式引用对象存在危险。在更实际的层面上,使用C ++ 11编译器和标准库,传递值应该导致移动而不是复制构造,并且无论如何几乎都是免费的。但是,通过引用传递会使调试变得更加容易,因为您不会反复进入shared_ptr的构造函数。

  • 使用shared_ptr而非std::make_sharednew()的构造函数构建您的shared_ptr

    shared_ptr<Parent> parent = std::make_shared<Parent>();

    使用现代编译器和库,可以保存对new()的调用。

  • shared_ptrweak_ptr都可以包含NULL - 就像任何其他指针一样。你应该养成在解除引用之前进行检查的习惯,并且可能也会assert()。对于构造函数的情况,您始终可以接受NULL指针,而是抛弃使用点。

  • 您可以考虑使用typedef作为共享指针类型。有时使用的一种风格如下:

    typedef std::weak_ptr<Parent> Parent_P;

    typedef std::shared_ptr<Parent> Parent_WkP;

    typedef std::weak_ptr<Child> Child_P;

    typedef std::shared_ptr<Child> Child_WkP;

  • 了解在头文件中,您可以转发声明shared_ptr<Type>而无需查看Type的完整声明,这也很有用。这可以节省很多标题膨胀

答案 1 :(得分:1)

使用共享指针的方式是正确的2个警告。

  1. 您的父母和孩子的树必须与其他对象共享指针的生命周期。如果您的父子树将是指针的唯一用户,请使用unique_ptr。如果另一个对象控制指针的生命周期,你只想引用指针,你可能最好使用weak_ptr,除非保证生命周期超过你的父子树,原始指针可能是合适的..请请记住,使用shared_ptr,您可以获得循环引用,因此它不是银弹。

  2. 至于如何控制NULL指针:这一切都归结为API中隐含的契约。如果不允许用户提供空指针,则只需记录此事实。执行此操作的最佳方法是包含指针不为空的assert。这将使您的应用程序在调试模式下崩溃(如果指针为空)但不会对您的发布二进制文件造成运行时损失。但是,如果空指针由于某种原因是允许的输入,则在空指针的情况下需要提供正确的错误处理。

答案 2 :(得分:1)

孩子不拥有父母。相反,它是相反的方式。如果孩子需要能够得到他们的父母,那么使用非拥有指针或参考。使用共享(或更好,如果可以的话,唯一的)指向父对子的指针。