`enable_shared_from_this`的作用是什么?

423

我在阅读 Boost.Asio 的示例时遇到了 enable_shared_from_this,但在阅读文档后仍然不知道该如何正确使用它。可以请问有人能给我一个例子和解释,在何时使用此类才是明智的吗。


1
这里有一个简单的解释:https://en.cppreference.com/w/cpp/memory/enable_shared_from_this - Fedor
6个回答

435
它使您能够在只有this的情况下获得有效的shared_ptr实例。如果没有它,您将无法获得shared_ptrthis,除非您已经将其作为成员拥有。以下示例来自于boost文档中关于enable_shared_from_this的内容
class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

f() 方法返回一个有效的shared_ptr,即使它没有成员实例。请注意,您不能简单地这样做:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

这个返回的共享指针将与“正确”的指针有不同的引用计数,其中一个在对象被删除时将失去并持有悬空引用。 enable_shared_from_this 已成为 C++ 11 标准的一部分。您也可以从那里以及 boost 中获取它。

243
关键点在于,仅仅返回"shared_ptr<Y>(this)"这个"显而易见"的技巧是行不通的,因为这将创建多个独立的shared_ptr对象并具有不同的引用计数。因此,您绝不能从相同的原始指针创建多个shared_ptr。 - j_random_hacker
7
需要注意的是,在_C++11及以后_,如果继承自std::enable_shared_from_this,在原始指针上使用std::shared_ptr构造函数是完全有效的。我不知道Boost的语义是否已更新以支持此功能。 - Matthew
7
@MatthewHolder,你有相关的报价吗?我在cppreference.com上读到了这样一句话:“构造一个已经由另一个std::shared_ptr管理的对象的std::shared_ptr将不会查询存储的弱引用,因此会导致未定义的行为。”(http://en.cppreference.com/w/cpp/memory/enable_shared_from_this) - Thorbjørn Lindeijer
14
为什么你不能只是用shared_ptr<Y> q = p呢? - Dan M.
3
@DanM. 你可以这样做,这就是为什么这个示例不是非常有用。虽然如此,它确实有它的用途。当没有 q 而你需要从类内部获取 p 时,这将会很有用。 - Hatted Rooster
显示剩余6条评论

284

根据Dr Dobbs关于弱引用的文章,我认为以下这个例子更容易理解(来源:http://drdobbs.com/cpp/184402026):

...像这样的代码将无法正确工作:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

两个 shared_ptr 对象都不知道对方的存在,因此当它们被销毁时,都会尝试释放资源。通常会导致问题。

同样地,如果成员函数需要一个拥有正在调用对象的对象的 shared_ptr,它不能随意创建一个对象:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

这段代码与之前的示例存在相同的问题,尽管形式更加微妙。当构造函数被调用时,shared_ptr对象sp1拥有新分配的资源。成员函数S::dangerous内部的代码不知道这个shared_ptr对象,因此它返回的shared_ptr对象与sp1不同。复制新的shared_ptr对象到sp2是没有帮助的;当sp2超出范围时,它将释放资源,并且当sp1超出范围时,它将再次释放资源。
避免这个问题的方法是使用类模板enable_shared_from_this。该模板需要一个模板类型参数,即定义所管理资源类的名称。该类必须公开派生自该模板,如下所示:
struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

在执行此操作时,请记住调用 shared_from_this 的对象必须由 shared_ptr 对象拥有。以下方式不起作用:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}

26
谢谢,这比目前被接受的答案更好地说明了问题的解决方法。 - goertzenator
2
+1:好答案。顺便提一下,与其使用“shared_ptr<S> sp1(new S);”,可能更好的方法是使用“shared_ptr<S> sp1 = make_shared<S>();”,例如参见https://dev59.com/fGMl5IYBdhLWcg3wkXpx - Arun
9
我相信最后一行应该是 shared_ptr<S> sp2 = p->not_dangerous();,因为这里的陷阱是在第一次调用shared_from_this()之前必须按照普通方式创建shared_ptr! 这真的很容易出错!在C++17之前,如果没有按照正常方式创建shared_ptr:auto sptr = std::make_shared<S>(); 或者 shared_ptr<S> sptr(new S());,调用shared_from_this()就会导致未定义的行为(UB)。值得庆幸的是,从C++17开始,这样做将抛出异常。 - AnorZaken
2
不好的例子:S* s = new S(); shared_ptr<S> ptr = s->not_dangerous(); <-- 允许在之前共享的对象上调用shared_from_this,即由std::shared_ptr <T>管理的对象。否则,行为未定义(直到C++17),会抛出std::bad_weak_ptr(通过从默认构造的weak_this调用shared_ptr构造函数)(自C++17以来) 。(http://en.cppreference.com/w/cpp/memory/enable_shared_from_this)。所以实际上它应该被称为`always_dangerous()`,因为你需要知道它是否已经被共享。 - AnorZaken
2
@AnorZaken 很好的观点。如果您提交了编辑请求来进行修复,那将非常有用。我已经这样做了。另一个有用的事情是,发布者不要选择主观、上下文敏感的方法名称! - underscore_d
显示剩余3条评论

39
以下是我的解释,从底层的角度来看(顶部回答对我来说不太清楚)。 *请注意,这是通过调查附带于Visual Studio 2012的shared_ptr和enable_shared_from_this源代码的结果。也许其他编译器实现enable_shared_from_this的方式不同...*
enable_shared_from_this向T添加一个private weak_ptr实例,该实例保存T的“唯一真实引用计数”。
因此,当您首次创建shared_ptr时,其内部weak_ptr的refcount将初始化为1。新的shared_ptr基本上支持此weak_ptr。
然后,T可以在其方法中调用shared_from_this以获取一个shared_ptr实例,该实例“依赖于相同的内部存储引用计数”。这样,您总是有一个地方存储T*的引用计数,而不是有多个shared_ptr实例互相不知道,并且每个实例都认为它是负责T的引用计数和在其引用计数达到零时删除它的shared_ptr。

2
这是正确的,而真正重要的部分是So, when you first create...,因为这是一个要求(就像你所说的,直到将对象指针传递到shared_ptr ctor中,weak_ptr才会初始化!),如果不小心处理这个要求,事情可能会变得非常糟糕。如果在调用shared_from_this之前没有创建任何shared_ptr,则会出现UB-同样,如果创建了多个shared_ptr,则也会出现UB。您必须确保只创建一个shared_ptr。 - AnorZaken
3
换句话说,“enable_shared_from_this”的整个想法本身就很脆弱,因为其目的是从“T”获取一个“shared_ptr<T>”,但实际上,当你得到一个指针“T t”时,通常不安全假设它已经被共享或没有被共享,猜测错误可能导致未定义行为。 - AnorZaken
“_internal weak_ptr gets initialized with a refcount of 1_” 中的 weak_ptr 是指向 T 的非拥有智能指针。weak_ptr 是一个拥有足够信息以创建其他拥有指针“副本”的拥有智能引用。weak_ptr 没有引用计数,但像所有拥有引用一样,它可以访问引用计数。 - curiousguy

21

我发现enable_shared_from_this非常有用的一个特定场景是:在使用异步回调时保证线程安全。

想象一下,类Client拥有一个类型为AsynchronousPeriodicTimer的成员:

struct AsynchronousPeriodicTimer
{
    // call this periodically on some thread...
    void SetCallback(std::function<void(void)> callback); 
    void ClearCallback(); // clears the callback
}

struct Client
{
    Client(std::shared_ptr< AsynchronousPeriodicTimer> timer) 
        : _timer(timer)

    {
        _timer->SetCallback(
            [this]
            () 
            {
                assert(this); // what if 'this' is already dead because ~Client() has been called?
                std::cout << ++_counter << '\n';
            }
            );
    }
    ~Client()
    {
        // clearing the callback is not in sync with the timer, and can actually occur while the callback code is running
        _timer->ClearCallback();
    }
    int _counter = 0;
    std::shared_ptr< AsynchronousPeriodicTimer> _timer;
}

int main()
{
    auto timer = std::make_shared<AsynchronousPeriodicTimer>();
    {
        auto client = std::make_shared<Client>(timer);
        // .. some code    
        // client dies here, there is a race between the client callback and the client destructor           
    }
}
客户端类将回调函数订阅到周期计时器上。一旦客户端对象超出作用域,客户端的回调函数和析构函数之间存在竞争条件。回调函数可能会使用悬空指针被调用!解决办法:使用enable_shared_from_this在回调调用期间扩展对象生命周期。
struct Client : std::enable_shared_from_this<Client>
{
Client(std::shared_ptr< AsynchronousPeriodicTimer> timer) 
    : _timer(timer)

    {

    }

    void Init()
    {
        auto captured_self = weak_from_this(); // weak_ptr to avoid cyclic references with shared_ptr

        _timer->SetCallback(
        [captured_self]
        () 
        {
            if (auto self = captured_self.lock())
            {
                // 'this' is guaranteed to be non-nullptr. we managed to promote captured_self to a shared_ptr           
                std::cout << ++self->_counter << '\n';
            }

        }
        );
    }
    ~Client()
    {
        // the destructor cannot be called while the callback is running. shared_ptr guarantees this
        _timer->ClearCallback();
    
    }
    int _counter = 0;
    std::shared_ptr< AsynchronousPeriodicTimer> _timer;
}
enable_shared_from_this机制与std::shared_ptr的内在线程安全性相结合,使我们能够确保在回调代码访问其内部成员时不能销毁Client对象。请注意,Init方法与构造函数分离,因为在构造函数退出之前enable_shared_from_this的初始化过程尚未完成。因此需要额外的方法。从构造函数中订阅异步回调通常是不安全的,因为回调可能访问未初始化的字段。

1
@Scylardor 想象一下,在计时器回调运行期间,析构函数在主线程中被调用。回调可能会访问已销毁的“this”。实际上,回调的清除既不是原子操作,也不与计时器同步。 - Elad Maimoni
1
哦,好的,谢谢你澄清这一点。我忘记了这个的多线程影响。现在很有道理了。很棒的例子! - Scylardor
1
我会投票支持这个答案。它清楚地回答了问题:为什么enable_shared_from_this有用?其他答案只是试图解释enable_shared_from_this的作用。 - cyb70289
1
@cyb70289 注意,我只是修复了一个小错误。很高兴它有帮助。 - Elad Maimoni
在这个例子中,捕获客户端的weak_ptr真的有用吗?weak_ptr不会增加引用计数。我认为客户端的析构函数仍然会在作用域消失之前被调用。 - clay
显示剩余2条评论

4

在c++11及以后版本中,这个功能完全相同:它使能够返回 this 作为共享指针,因为 this 给出了一个原始指针。

换句话说,它允许您将代码转换为以下形式:

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

转化为下面的形式:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           

只有在这些对象始终由shared_ptr管理时,此方法才能正常工作。您可能需要更改接口以确保这种情况。 - curiousguy
1
你是绝对正确的,@curiousguy。这是不言而喻的。我也喜欢使用typedef来改进公共API的可读性,以便将所有shared_ptr定义为其别名类型。例如,我通常会将std::shared_ptr<Node> getParent const()公开为NodePtr getParent const()。如果你确实需要访问内部原始指针(最好的例子:处理C库),那么可以使用std::shared_ptr<T>::get,虽然我很讨厌提到它,因为我已经看到过太多错误的情况使用这个原始指针访问器。 - mchiasson

4
请注意,使用boost :: intrusive_ptr不会遇到这个问题。这通常是解决此问题的更方便的方法。

是的,但 enable_shared_from_this 允许您使用专门接受 shared_ptr<> 的 API 进行工作。在我看来,这样的 API 通常是 错误的(因为最好让堆栈中更高层次的东西拥有内存),但如果您被迫使用这样的 API,则这是一个不错的选择。 - cdunn2001
2
尽可能地遵循标准会更好。 - Sergei

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