I frequently see this pattern in code, binding shared_from_this
as the first parameter to a member function and dispatching the result using an async_*
function. Here's an example from another question:
void Connection::Receive()
{
boost::asio::async_read(socket_,boost::asio::buffer(this->read_buffer_),
boost::bind(&Connection::handle_Receive,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
The only reason to use shared_from_this()
instead of this
is to keep the object alive until the member function gets called. But unless there's some kind of boost magic somewhere, since the this
pointer is of type Connection*
, that's all handle_Receive
can take, and the smart pointer returned should be converted to a regular pointer immediately. If that happens, there's nothing to keep the object alive. And, of course, there's no pointer in calling shared_from_this
.
However, I've seen this pattern so often, I can't believe it's as completely broken as it seems to me. Is there some Boost magic that causes the shared_ptr to be converted to a regular pointer later, when the operation completes? If so, is this documented somewhere?
In particular, is it documented somewhere that the shared pointer will remain in existence until the operation completes? Calling get_pointer
on the strong pointer and then calling the member function on the returned pointer is not sufficient unless the strong pointer isn't destroyed until the member function returns.
shared_ptr is now part of the C++11 Standard, as std::shared_ptr . Starting with Boost release 1.53, shared_ptr can be used to hold a pointer to a dynamically allocated array. This is accomplished by using an array type ( T[] or T[N] ) as the template parameter.
The shared_ptr type is a smart pointer in the C++ standard library that is designed for scenarios in which more than one owner might have to manage the lifetime of the object in memory.
Admittedly, the std::shared_ptr is about two times slower than new and delete. Even std::make_shared has a performance overhead of about 10%.
The purpose of shared_ptr is to manage an object that no one "person" has the right or responsibility to delete, because there could be others sharing ownership. So you shouldn't ever want to, either.
In short, boost::bind
creates a copy of the boost::shared_ptr<Connection>
that is returned from shared_from_this()
, and boost::asio
may create a copy of the handler. The copy of the handler will remain alive until one of the following occurs:
run()
, run_one()
, poll()
or poll_one()
member function has been invoked.io_service
is destroyed.io_service::service
that owns the handler is shutdown via shutdown_service()
.Here are the relevant excerpts from the documentation:
boost::bind documentation:
The arguments that
bind
takes are copied and held internally by the returned function object.
boost::asio io_service::post
:
The
io_service
guarantees that the handler will only be called in a thread in which therun()
,run_one()
,poll()
orpoll_one()
member functions is currently being invoked. [...] Theio_service
will make a copy of the handler object as required.
boost::asio io_service::~io_service
:
Uninvoked handler objects that were scheduled for deferred invocation on the
io_service
, or any associated strand, are destroyed.
Where an object's lifetime is tied to the lifetime of a connection (or some other sequence of asynchronous operations), ashared_ptr
to the object would be bound into the handlers for all asynchronous operations associated with it. [...] When a single connection ends, all associated asynchronous operations complete. The corresponding handler objects are destroyed, and allshared_ptr
references to the objects are destroyed.
While dated (2007), the Networking Library Proposal for TR2 (Revision 1) was derived from Boost.Asio. Section 5.3.2.7. Requirements on asynchronous operations
provides some details for the arguments to async_
functions:
In this clause, an asynchronous operation is initiated by a function that is named with the prefix
async_
. These functions shall be known as initiating functions. [...] The library implementation may make copies of the handler argument, and the original handler argument and all copies are interchangeable.The lifetime of arguments to initiating functions shall be treated as follows:
- If the parameter is declared as a const reference or by-value [...] the implementation may make copies of the argument, and all copies shall be destroyed no later than immediately after invocation of the handler.
[...] Any calls made by the library implementation to functions associated with the initiating function's arguments will be performed such that calls occur in a sequence call1 to calln, where for all i, 1 ≤ i < n, calli precedes call i+1.
Thus:
shared_ptr<Connection>
, increasing the reference count of the Connection
instance while the copies of handler remain alive.io_serive::service
is shutdown or the io_service
is destroyed. In the example, the copies of handler will be destroyed, decreasing the reference count of Connection
, and potentially causing the Connection
instance to be destroyed.Connection
, and potentially causing it to be destroyed.asnyc_
's arguments, will be executed sequentially, and not concurrent. This includes io_handler_deallocate
and io_handler_invoke
. This guarantees that the handler will not be deallocated while the handler is being invoked. In most areas of the boost::asio
implementation, the handler is copied or moved to stack variables, allowing the destruction to occur once execution exits the block in which it was declared. In the example, this ensures that the reference count for Connection
will be at least one during the invocation of the handler.It goes like this:
1) Boost.Bind documentation states:
"[Note: mem_fn creates function objects that are able to accept a pointer, a reference, or a smart pointer to an object as its first argument; for additional information, see the mem_fn documentation.]"
2) mem_fn documentation says:
When the function object is invoked with a first argument x that is neither a pointer nor a reference to the appropriate class (X in the example above), it uses get_pointer(x) to obtain a pointer from x. Library authors can "register" their smart pointer classes by supplying an appropriate get_pointer overload, allowing mem_fn to recognize and support them.
So, pointer or smart-pointer is stored in the binder as-is, until its invocation.
I also see this pattern used a lot and (thanks to @Tanner) I can see why it's used when the io_service
is run in multiple threads. However, I think that there are still lifetime issues with it as it replaces a potential crash with a potential memory/resource leak...
Thanks to boost::bind, any callbacks that are bound to shared_ptrs become "users" of the object (increasing the objects use_count), so the object won't be deleted until all of the outstanding callbacks have been called.
The callbacks to the boost::asio::async* functions are called whenever, cancel or close is called on the relevant timer or socket. Normally you would just make the appropriate cancel/close calls in the destructor using Stroustrup's beloved RAII pattern; job done.
However, the destructor won't be called when the owner deletes the object, because the callbacks still hold copies of the shared_ptrs and so their use_count will be greater than zero, resulting in a resource leak. The leak can be avoided by making the appropriate cancel/close calls prior to deleting the object. But it’s not as fool-proof as using RAII and making the cancel/close calls in the destructor. Ensuring that the resources are always freed, even in the presence of exceptions.
An RAII conforming pattern is to use static functions for callbacks and pass a weak_ptr to boost::bind when registering the callback function as in the example below:
class Connection : public boost::enable_shared_from_this<Connection>
{
boost::asio::ip::tcp::socket socket_;
boost::asio::strand strand_;
/// shared pointer to a buffer, so that the buffer may outlive the Connection
boost::shared_ptr<std::vector<char> > read_buffer_;
void read_handler(boost::system::error_code const& error,
size_t bytes_transferred)
{
// process the read event as usual
}
/// Static callback function.
/// It ensures that the object still exists and the event is valid
/// before calling the read handler.
static void read_callback(boost::weak_ptr<Connection> ptr,
boost::system::error_code const& error,
size_t bytes_transferred,
boost::shared_ptr<std::vector<char> > /* read_buffer */)
{
boost::shared_ptr<Connection> pointer(ptr.lock());
if (pointer && (boost::asio::error::operation_aborted != error))
pointer->read_handler(error, bytes_transferred);
}
/// Private constructor to ensure the class is created as a shared_ptr.
explicit Connection(boost::asio::io_service& io_service) :
socket_(io_service),
strand_(io_service),
read_buffer_(new std::vector<char>())
{}
public:
/// Factory method to create an instance of this class.
static boost::shared_ptr<Connection> create(boost::asio::io_service& io_service)
{ return boost::shared_ptr<Connection>(new Connection(io_service)); }
/// Destructor, closes the socket to cancel the read callback (by
/// calling it with error = boost::asio::error::operation_aborted) and
/// free the weak_ptr held by the call to bind in the Receive function.
~Connection()
{ socket_.close(); }
/// Convert the shared_ptr to a weak_ptr in the call to bind
void Receive()
{
boost::asio::async_read(socket_, boost::asio::buffer(read_buffer_),
strand_.wrap(boost::bind(&Connection::read_callback,
boost::weak_ptr<Connection>(shared_from_this()),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred,
read_buffer_)));
}
};
Note: the read_buffer_
is stored as a shared_ptr
in the Connection class and passed to the read_callback
function as a shared_ptr
.
This is to ensure that where multiple io_services
are run in separate tasks, the read_buffer_
is not deleted until after the other tasks have completed, i.e. when the read_callback
function has been called.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With