13

According to http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/Handler.html, a handler provided to io_service::post must be copy constructible.

However, this excludes a scenario where a socket is accepted, and the response handler is moved, guaranteeing me that there is only one handler for the job:

auto socket = std::make_unique<socket>();
accepter.accept(*socket);

service.post([s{std::move(socket)}] {
  asio::write(*s, buffer("response"), ignored_err);
});

So why is this copy constructible requirement?

-- EDIT --

Clarification: I would like to make my code assure that only one instance of a handler is present. This makes it a lot easier to reason about it.

For a lot of programs, CopyConstructible is too strict a requirement, MoveConstructible is more appropriate.

    auto i = std::make_unique<int>();
    auto handler_w_resource = [i{std::move(i)}]{ ++(*i);};
    // auto copied = handler_w_resource; --> +1: lambda can't be copied!
    auto moved = std::move(handler_w_resource);

Therefor I was quite surprised that it could not be moved in:

    service.post(std::move(moved)); // :( post accepts no rvalue
xtofl
  • 40,723
  • 12
  • 105
  • 192
  • Does the unique pointer work? Tbh I haven't tested my 1.60 build, but at least till 1.56, socket had to be in shared pointer... – Nim Jun 08 '16 at 19:39
  • Is it required if you use standalone ASIO? I have been using standalone ASIO at C++11 standard for some time and some C++11-only perks, like move-semantics and std::function, work like I would expect. The copy-constructibility might be required because boost::asio must support C++03, and it might be too complicated to say "copy constructible if C++03, but only move constructible required if C++11". Just a guess, I don't know for sure. – Chris Beck Jun 08 '16 at 20:01
  • 1
    seems they lowered the requirements for Handlers to be MoveConstructible with boost v1.67 [reference](https://www.boost.org/doc/libs/1_67_0/doc/html/boost_asio/reference/Handler.html) – Ivan_a_bit_Ukrainivan May 21 '18 at 07:27

2 Answers2

9

I cannot identify any material that explains the reasoning as to why handlers are required to be CopyConstructible, but it is explicitly noted even in the presence of C++11 support. The Movable Handlers documentation notes:

As an optimisation, user-defined completion handlers may provide move constructors, and Boost.Asio's implementation will use a handler's move constructor in preference to its copy constructor. In certain circumstances, Boost.Asio may be able to eliminate all calls to a handler's copy constructor. However, handler types are still required to be copy constructible.

The type checking Asio performs allows for friendlier compiler error messages when a type requirement is not satisfied. This type checking occurs earlier in the call stack, and is not predicated on if the use of the object would generate a compiler error. For example, when type checking is enabled, type checking will emit a compiler error for a handler that does not have a copy constructor, even if all calls to the handler's copy constructor have been eliminated. By defining BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS, one can disable the explicit type checking, and allow for compiler errors to surface from call points deeper in the implementation if they occur. The History notes this option:

Asio 1.6.0 / Boost 1.47

  • ...
  • Added friendlier compiler errors for when a completion handler does not meet the necessary type requirements. When C++0x is available (currently supported for g++ 4.5 or later, and MSVC 10), static_assert is also used to generate an informative error message. This checking may be disabled by defining BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS.

Here is a complete example demonstrating this functionality. Within it, ownership of a socket managed by unique_ptr is transferred to a handler via std::move():

#include <functional> // std::bind
#include <memory>     // std::unique_ptr
#include <string>     // std::string
#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
#include <boost/asio.hpp>

const auto noop = std::bind([]{});

int main()
{
  using boost::asio::ip::tcp;  

  // Create all I/O objects.
  boost::asio::io_service io_service;
  tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
  auto socket1 = std::make_unique<tcp::socket>(std::ref(io_service));
  tcp::socket socket2(io_service);

  // Connect the sockets.
  acceptor.async_accept(*socket1, noop);
  socket2.async_connect(acceptor.local_endpoint(), noop);
  io_service.run();
  io_service.reset();

  // Move ownership of socket1 to a handler that will write to the
  // socket.
  const std::string expected_message = "test message";
  io_service.post([socket1{std::move(socket1)}, &expected_message] {
    boost::asio::write(*socket1, boost::asio::buffer(expected_message));
  });
  io_service.run();

  // Read from socket2.
  std::vector<char> actual_message(socket2.available());
  boost::asio::read(socket2, boost::asio::buffer(actual_message));

  // Verify message.
  assert(std::equal(
    begin(expected_message), end(expected_message),
    begin(actual_message), end(actual_message)));
}
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • 5
    For what it is worth, in the current [networking-ts draft](https://github.com/cplusplus/networking-ts), the type `CompletionHandler` must satisfy the requirements of `Destructible` and `MoveConstructible`. – Tanner Sansbury Jun 13 '16 at 17:19
-1

It's just a convention.

The convention can be found throughout many C++ libraries, including all your standard library algorithms.

The convention makes it a lot easier to

  • implement the semantics correctly
  • know what to expect
  • let the compiler do deep optimizations (references kill many optimizations due the aliasing problem)

So, if you need a handler by reference, just pass std::ref(f). It was designed for the purpose.


Beware of lifetime issues - as always when using references.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    You sum up the three arguments _against_ copy constructibility. Exactly why I asked this question. In this case it's actually quite _unexpected_ that my handler would be copied. That would mean I can have two handlers for one socket. It would be easier to implement semantics correctly if the handler could be moved in, and the compiler would have more guarantees about the number of handlers lying around, so could optimize deeper. – xtofl Jun 10 '16 at 04:03
  • 2
    @xtofl I listed objective reasons. Are you saying the standard library algortihms have been designed to hurt performance? Did you actually compare generated assembly to see how compiler optimizes various approaches? The only logical explanation I can think of for your comment is "But! I have a expensive-to-copy class" or "But I want to avoid copying because I count instances for some other purpose". In which case you use that reference_wrapper. Problem solved. The point is: taking functions by value gives **you** the choice. – sehe Jun 10 '16 at 08:04
  • Sorry, I must miss something, but my issue is that I _can't_ choose since the postpostpost operation doesn't support move semantics. So I was rather expecting an answer like 'since Move Semantics weren't yet known at the time this library was designed'. In other words: whas this convention chosen before or after the introduction of move semantics? – xtofl Jun 10 '16 at 09:11
  • 1
    (The Asio idiomatic way to avoid "large completion handlers" is to use enable_shared_from_this on the object instance that the handler closes over. You can see a side-by-side here http://stackoverflow.com/a/32725856/85371. Of course you don't need to use shared_ptr's reference counting to manage lifetimes. If you do it some other way, `std::reference_wrapper<>` will do just fine) – sehe Jun 10 '16 at 09:20
  • 1
    @sehe: I think what you were missing is that xtofl *wants* value semantics, not reference semantics; he just wants to move the value around (efficiently) instead of copying it around. Your answer and comments boil down to "Just abandon value semantics and use reference semantics instead", which isn't what was being asked. – Quuxplusone Jul 05 '17 at 20:56
  • @Quuxplusone What makes you think I missed that? In the end it comes down to what the framework supports, and my answer is about that. Of course the "unofficial" move support is a reprieve there, see the accepted answer. – sehe Jul 05 '17 at 20:57