shared_from_this causing bad_weak_ptr
John Zwinck's essential analysis is spot on:
The bug is that you're using shared_from_this() on an object which has no shared_ptr pointing to it. This violates a precondition of shared_from_this(), namely that at least one shared_ptr must already have been created (and still exist) pointing to this.
However, his advice seems completely beside the point and dangerous in Asio code.
You should solve this by - indeed - not handling raw pointers to tcp_connection
in the first place but always using shared_ptr
instead.
boost::bind
has the awesome feature that it binds to shared_ptr<>
just fine so it automagically keeps the pointed to object alive as long as some asynchronous operation is operating on it.
This - in your sample code - means you don't need the clients
vector, going the opposite way from John's answer:
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();
}
}
I've included a sample that makes tcp_connection
do some trivial work (it loops writing 'hello world' to the client each second, until the client drops the connection. When it does, you can see the destructor of the tcp_connection
operation being run:
Live On 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";
}
}
Typical output:
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
// Do not forget to ----v---- publicly inherit :)
class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
The bug is that you're using shared_from_this()
on an object which has no shared_ptr
pointing to it. This violates a precondition of shared_from_this()
, namely that at least one shared_ptr
must already have been created (and still exist) pointing to this
.
The root cause of your troubles seems to be the fact that you're storing the result of new
in a raw pointer initially. You should store the result of new
in a smart pointer (always, basically). Perhaps you can store the smart pointer in your clients
list straight away, then.
Another approach which I mentioned in the comments is to stop using shared_from_this()
entirely. You don't need it. As for this bit of code you mentioned:
if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
{
clients.erase(shared_from_this());
}
You can replace it by:
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);
}
That is, create a "dumb" smart pointer which will never deallocate (https://stackoverflow.com/a/5233034/4323) but which will give you what you need to delete it from the list of clients. There are other ways to do it too, such as by searching the std::set
using a comparison function which takes one shared_ptr
and one raw pointer and knows to compare the addresses to which they point. It doesn't matter much which way you choose, but you escape the shared_from_this()
situation entirely.