shared_from_this导致bad_weak_ptr错误

76

我正在尝试在asio中保持已连接客户端的列表。 我从文档中调整了聊天服务器示例(http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/example/cpp03/chat/chat_server.cpp),以下是我最终得出的重要部分:

#include <iostream>
#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <set>

using boost::asio::ip::tcp;

class tcp_connection;

std::set<boost::shared_ptr<tcp_connection>> clients;

void add_client(boost::shared_ptr<tcp_connection> client)
{
    clients.insert(client);
}

class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
{
public:
    tcp_connection(boost::asio::io_service& io_service) : socket_(io_service)
    {
    }

    tcp::socket socket_;

    void start()
    {
        add_client(shared_from_this());
    }

    tcp::socket& socket()
    {
        return socket_;
    }
};

class tcp_server
{
public:
    tcp_server(boost::asio::io_service& io_service)
        : io_service_(io_service),
        acceptor_(io_service, tcp::endpoint(tcp::v4(), 6767))
    {
        tcp_connection* new_connection = new tcp_connection(io_service_);
        acceptor_.async_accept(new_connection->socket(),
                             boost::bind(&tcp_server::start_accept, this, new_connection,
                                         boost::asio::placeholders::error));
    }

private:
    void start_accept(tcp_connection* new_connection,
                      const boost::system::error_code& error)
    {
        if (!error)
        {
            new_connection->start();
            new_connection = new tcp_connection(io_service_);
            acceptor_.async_accept(new_connection->socket(),
                                   boost::bind(&tcp_server::start_accept, this, new_connection,
                                               boost::asio::placeholders::error));
        }
    }

    boost::asio::io_service& io_service_;
    tcp::acceptor acceptor_;
};

int main()
{
    try
    {
        boost::asio::io_service io_service;
        tcp_server server(io_service);
        io_service.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}

在调用shared_from_this()时,我的服务器崩溃并显示以下消息:

异常:tr1::bad_weak_ptr

我已经做了一些搜索,似乎shared_from_this()很特别,但我似乎找不到需要更改的确切内容。


1
为什么你要将new的结果存储在一个裸指针中,只是为了稍后使用shared_from_this()呢?看起来你的设计可以简化,从而完全消除这个问题。 - John Zwinck
4
enable_shared_from_this 的 Boost 文档中提到:“必须存在至少一个拥有 t 的 shared_ptr 实例 p”,而你似乎没有这个实例。 - Jonathan Potter
2
@chrisvj 我的理解是,在使用 shared_from_this 创建更多对象之前,您需要已经拥有一个持有该对象的 shared_ptr。虽然我自己从未使用过它,所以这只是一个猜测。 - Jonathan Potter
@JonathanPotter 嗯,不确定,在我没有的示例中没有涉及shared_ptr。 - chrisvj
1
只有在对象的生命周期由shared pointers管理时,调用shared_from_this才有意义。否则,不可能拥有一个lifetime保证至少与对象寿命一样长的shared pointer,而shared_from_this的唯一目的就是返回这样的指针。因此,总之,你正在要求shared_from_this去做不可能的事情。 - David Schwartz
显示剩余3条评论
4个回答

66

John Zwinck的基本分析是准确的:

问题在于你正在使用shared_from_this()来操作一个没有指向它的shared_ptr的对象。这违反了shared_from_this()的前提条件,即必须已经创建(并仍然存在)至少一个shared_ptr指向此对象。

然而,在Asio代码中,他的建议似乎完全无关紧要且危险。

您应该通过确实不使用原始指针来处理tcp_connection,而始终改用shared_ptr来解决此问题。

boost::bind具有很棒的功能,可以很好地绑定到shared_ptr<>,因此它会自动保持指向的对象在某些异步操作正在对其进行操作时一直处于活动状态。

这意味着-在您的示例代码中-您不需要clients向量,与John的答案相反:

void start_accept()
{
    tcp_connection::sptr new_connection = boost::make_shared<tcp_connection>(io_service_);
    acceptor_.async_accept(new_connection->socket(),
            boost::bind(
                &tcp_server::handle_accept,
                this, new_connection, asio::placeholders::error
            )
        );
}

void handle_accept(tcp_connection::sptr client, boost::system::error_code const& error)
{
    if (!error)
    {
        client->start();
        start_accept();
    }
}

我已经包含了一个示例,使得tcp_connection执行一些琐碎的工作(它会每秒向客户端写入“hello world”,直到客户端断开连接。当客户端断开连接时,您可以看到tcp_connection操作的析构函数被运行:

在 Coliru 上实时演示

#include <iostream>
#include <boost/bind.hpp>
#include <boost/make_shared.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
{
public:
    typedef boost::shared_ptr<tcp_connection> sptr;

    tcp_connection(asio::io_service& io_service) : socket_(io_service), timer_(io_service)
    {
    }

    void start()
    {
        std::cout << "Created tcp_connection session\n";

        // post some work bound to this object; if you don't, the client gets
        // 'garbage collected' as the ref count goes to zero
        do_hello();
    }

    ~tcp_connection() {
        std::cout << "Destroyed tcp_connection\n";
    }

    tcp::socket& socket()
    {
        return socket_;
    }

  private:
    tcp::socket socket_;
    asio::deadline_timer timer_;

    void do_hello(boost::system::error_code const& ec = {}) {
        if (!ec) {
            asio::async_write(socket_, asio::buffer("Hello world\n"),
                    boost::bind(&tcp_connection::handle_written, shared_from_this(), asio::placeholders::error, asio::placeholders::bytes_transferred)
                );
        }
    }

    void handle_written(boost::system::error_code const& ec, size_t /*bytes_transferred*/) {
        if (!ec) {
            timer_.expires_from_now(boost::posix_time::seconds(1));
            timer_.async_wait(boost::bind(&tcp_connection::do_hello, shared_from_this(), asio::placeholders::error));
        }
    }
};

class tcp_server
{
public:
    tcp_server(asio::io_service& io_service)
        : io_service_(io_service),
          acceptor_(io_service, tcp::endpoint(tcp::v4(), 6767))
    {
        start_accept();
    }

private:
    void start_accept()
    {
        tcp_connection::sptr new_connection = boost::make_shared<tcp_connection>(io_service_);
        acceptor_.async_accept(new_connection->socket(),
                boost::bind(
                    &tcp_server::handle_accept,
                    this, new_connection, asio::placeholders::error
                )
            );
    }

    void handle_accept(tcp_connection::sptr client, boost::system::error_code const& error)
    {
        if (!error)
        {
            client->start();
            start_accept();
        }
    }

    asio::io_service& io_service_;
    tcp::acceptor acceptor_;
};

int main()
{
    try
    {
        asio::io_service io_service;
        tcp_server server(io_service);

        boost::thread(boost::bind(&asio::io_service::run, &io_service)).detach();

        boost::this_thread::sleep_for(boost::chrono::seconds(4));
        io_service.stop();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }
}

典型输出:
sehe@desktop:/tmp$ time (./test& (for a in {1..4}; do nc 127.0.0.1 6767& done | nl&); sleep 2; killall nc; wait)
Created tcp_connection session
Created tcp_connection session
     1  Hello world
Created tcp_connection session
     2  Hello world
Created tcp_connection session
     3  Hello world
     4  Hello world
     5  Hello world
     6  Hello world
     7  Hello world
     8  Hello world
     9  Hello world
    10  Hello world
    11  Hello world
    12  Hello world
    13  
Destroyed tcp_connection
Destroyed tcp_connection
Destroyed tcp_connection
Destroyed tcp_connection
Destroyed tcp_connection

real    0m4.003s
user    0m0.000s
sys 0m0.015s

26
因为我忘记在“std::enable_shared_from_this<type>()”前面加上“public”,所以出现了“bad_weak_ptr”异常。 - Rostfrei
@Rostfrei 是的,这经常发生 - 我一直在努力寻找“规范”的答案,但我意识到很难找到,因为症状令人惊讶,而且很容易被忽略。 - sehe
@Rostfrei 谢谢,这是一条重要的提示!我曾经苦恼了30分钟,因为我认为public不是必需的... - Zdravko Donev
2
@Rostfrei 非常感谢!我已经疯狂调试了两个小时,结果是因为缺少了 public - CSawy
1
@ThekoLekena,我已经为您应用了[c++-faq]标签。 - sehe
显示剩余4条评论

28
// Do not forget to ----v---- publicly inherit :)
class tcp_connection : public boost::enable_shared_from_this<tcp_connection>

对于像我这样想知道为什么的人:https://dev59.com/PFkS5IYBdhLWcg3wYl3V - Andrei
哇,我感觉好傻,这个东西上几次我尝试使用 shared_from_this() 的时候本可以省掉许多麻烦的。 - itmuckel

15

这个问题在于你正在使用shared_from_this(),但是该对象没有任何指向它的shared_ptr。 这违反了shared_from_this()的前提条件,即必须至少已经创建了(并仍然存在)指向this的一个shared_ptr

你的问题根源似乎在于最初将new的结果存储在原始指针中。你应该始终将new的结果存储在智能指针中(基本上总是)。也许你可以直接将智能指针存储在clients列表中。

我在评论中提到的另一种方法是完全停止使用shared_from_this()。你并不需要它。至于你提到的代码:

if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
{
    clients.erase(shared_from_this());
}

您可以将其替换为:

if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
{
    boost::shared_ptr<tcp_connection> victim(this, boost::serialization::null_deleter());
    clients.erase(victim);
}

也就是说,创建一个“哑”智能指针,它永远不会释放内存 (https://dev59.com/KXE85IYBdhLWcg3wkkfK#5233034),但会给你删除客户端列表中的指针所需的信息。还有其他方法可以实现,例如使用一个比较函数搜索 std::set,该函数接受一个 shared_ptr 和一个裸指针,并知道如何比较它们所指向的地址。选择哪种方式并不重要,但可以完全避免使用 shared_from_this()


2
shared_from_this "情况"在Boost Asio中是会话管理的惯用语。它的目的是使异步会话的生命周期管理更加容易。实际上,这里的clients集合使得这变得非常困难,因为在所有异步操作可能失败的地方正确清理几乎是不可能的(甚至没有考虑一般的异常安全性)。 - sehe

1

这里的答案很棒,揭示了我的问题的解决方案。不过:

  1. 我搜索了“bad_weak_ptr shared_from_this”,但即使我没有提到boost,这仍然是搜索结果中排名最高的。我使用的是标准C++17。
  2. 问答者给出的是一个具体的例子,有点复杂,你必须深入挖掘一些无关的boost socket代码才能找到核心问题。

鉴于这两点,我认为发布一个MRE(最小可重现示例),并说明需要修复的问题,可能会有所帮助。以下代码来源于cppreference

有问题的代码

#include <iostream>
#include <memory>
 
struct Foo : public std::enable_shared_from_this<Foo> {
    Foo() { std::cout << "Foo::Foo\n"; }
    ~Foo() { std::cout << "Foo::~Foo\n"; } 
    std::shared_ptr<Foo> getFoo() { return shared_from_this(); }
};
 
int main()
{
    try
    {
        Foo *f = new Foo;
        // Oops! this throws std::bad_weak_ptr. f is a raw pointer to Foo (not
        // managed by a shared pointer), and calling getFoo tries to create a
        // shared_ptr from the internal weak_ptr. The internal weak_ptr is nullptr,
        // so this cannot be done, and the exception is thrown. Note, the
        // cppreference link above says that prior to C++17, trying to do this
        // is undefined behavior. However, in my testing on godbolt, an exception
        // is thrown for C++11, 14, and 17 with gcc.
        std::shared_ptr<Foo> sp = f->getFoo();
    }
    catch(const std::bad_weak_ptr& bwp)
    {
        // the exception is caught, and "bad_weak_ptr" is printed to stdout
        std::cout << bwp.what();
        exit(-1);
    }

    return 0;
}

输出:

Foo::Foo
bad_weak_ptr

潜在解决方案

确保f是由shared_ptr管理的:

try
{
    Foo *f = new Foo;
    // this time, introduce a shared pointer
    std::shared_ptr<Foo> sp(f);
    // now, f is managed by a shared pointer. Its internal weak_ptr is valid,
    // and so the retrieval of a shared_ptr via shared_from_this works as
    // desired. We can get a weak_ptr or a shared_ptr
    std::weak_ptr<Foo> wp = f->getFoo();
    std::shared_ptr<Foo> sp2 = f->getFoo();
    // all pointers go out of scope and the Foo object is deleted once
    // the reference count reaches 0
}
catch(const std::bad_weak_ptr& bwp)
{
    std::cout << bwp.what();
    exit(-1);
}

输出:

Foo::Foo
Foo::~Foo

正如其他答案所述并如上所示,您必须在调用shared_from_this之前拥有由共享指针管理的对象,否则您将获得bad_weak_ptr异常或UB,具体取决于您的C++标准。这里有一个playground,供任何感兴趣的人使用。


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