Reputation: 73
I've noticed 3 main options to call handler for async operations in Boost.Asio:
class MyClass {
public:
void doReadFromSocket(); //implementation options provided below.
private:
void handleRead(const boost::system::error_code& ec,
std::size_t bytesTransferred) {
// ... handle async_read result.
}
boost::asio::streambuf m_buffer;
boost::asio::ip::tcp::socket m_socket;
}
Option 1 - use lambda function:
void MyClass::doReadFromSocket() {
boost::asio::async_read(m_socket, m_bufferIn,
boost::asio::transfer_at_least(1),
[this](const boost::system::error_code& ec,
std::size_t bytesTransferred) {
// ...
}
}
Option 2 - use direct member function call: UPD. The option is not correct, as pointed out in the comments.
void MyClass::doReadFromSocket() {
boost::asio::async_read(m_socket, m_bufferIn,
boost::asio::transfer_at_least(1),
handleRead);
}
Option 3 - use boost::bind to call member function that handles the read:
void MyClass::doReadFromSocket() {
boost::asio::async_read(m_socket, m_bufferIn,
boost::asio::transfer_at_least(1),
boost::bind(&MyClass::handleRead, this, _1, _2));
}
}
The main questions are:
boost::bind
to handle the read? Why not use a direct member function call? A lot of examples in the boost library use boost::bind
to handle async operations. Also, this option seems the least convenient from my perspectiveUpvotes: 1
Views: 597
Reputation: 392893
Like others said, not all of your options are correct. Looking beyond that:
bind
(std
or boost
) has the added effect of returning a bind-expression templated on the original functor type. In C++ this means that the original associated namespaces for ADL still apply to the bound handler. This property is important when e.g. the associated executor, allocator or cancellation slot are important (like, a strand).
The inverse of this is a common pitfall, e.g. when using std::function<>
instead of a bind expression: boost::asio::bind_executor does not execute in strand
boost::bind has the advantage of being available on C++03 (which Asio supports)
lambdas have a slight benefit of reducing the number of explicit functions defined, at the cost of increased complexity; especially the lifetime of captured variables can be harder to check.
Also note that you might still need to manually associate an executor (asio::bind_executor
) with some lambda handlers.
The reason I feel lambdas have become more viable in generic Asio code is because in "recent" Asio versions IO objects are constructed from a specific executor, which will be the default executor for completion handlers passed to async_
initiation functions on these IO objects.
Note though that this scratches the surface of options. Contrary to what some people commented, the "handler" argument is not necessarily a handler. It's actually what's known as a completion Token. It can be many things, like use_future
, use_awaitable
, deferred
, detached
, as_tuple(another_token)
etc.
Also, there are design patterns where the handler itself is a class - e.g. derived from asio::coroutine
. This is often used inside the library because it provides a very lightweight way to provide "faux coroutine"-like state machines to implement asynchronous operations in a way that's compatible with C++03 and all completion tokens, without incurring unnecessary overhead.
Also note that many libraries use convenience helpers (like beast::bind_front_handler
) to reduce the amount of repeated code for bind expressions.
It's up to you, based on specific requirements and personal preference. Asio is a highly generic framework for asynchronous operations and you have choices on how you interact with it.
Upvotes: 4