如何在良好的c++ api中正确使用shared_ptr

How to properly use shared_ptr in good C++ APIs

本文关键字:shared ptr api c++      更新时间:2023-10-16

我目前正在努力找出如何在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?还是有更好的办法?也许是某种不可空的共享指针?

按照您所描述的目的使用共享指针是合理的,并且在c++ 11库中越来越普遍。

注意事项:

  • 在API上,将shared_ptr作为参数强制调用者构造shared_ptr。这绝对是一个很好的举动,有所有权的转移点。在函数只使用shared_ptr的情况下,可以接受对对象或shared_ptr

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

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

  • std::make_shared而不是new()shared_ptr的构造器构建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;

  • 知道在头文件中您可以在没有看到Type的完整声明的情况下转发声明shared_ptr<Type>也是有用的。这可以节省大量的头文件

使用共享指针的方式是正确的,但有两点需要注意。

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

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

子女不拥有父母。相反,情况正好相反。如果子节点需要能够获得它们的父节点,那么使用非拥有的指针或引用。使用共享的(如果可以的话最好是唯一的)父指针指向子指针。