Peniblec
Peniblec

Reputation: 457

Binding a handler for async operation to non-static member of another class

Here is the situation:

I have a Peer class which stores a boost::asio::ip::tcp::socket, and a Network class which has a set (vector/map/whatever) of Peers.

For the moment, Peer::listen() starts the async read, binding the completion of the event to Peer::handle_incoming_message. This means Peer has to look for the asio::error::eof to know whether the distant peer has disconnected.

Peer can't call Network::remove_peer() because I want this method to destroy the Peer object (haven't actually tried, but I guess returning from remove_peer would crash or something if the Peer object that called it has been destroyed). So right now all I'm doing is set the Peer object's state to PEER_STATE_DEAD, and periodically I check all the Peers inside the Network's set and remove all those with state PEER_STATE_DEAD.

I would like Network to have a handle_incoming_message([...], Peer* p) method, which would actually only check whether eof was raised and if not, call p->receive() which would read from the buffer given to async_read.

So ideally the Peer class would have a ListenHandler object which would be initialized to Network::handle_incoming_message([...], this) at construction (or shortly after). The thing is, I can't seem to compile with this setup.

Simplified code:

Network:

class Network: 
{
[...]
  void handle_incoming_message(const system::error_code& error,
                               Peer* emitter);
[...]
};

void Network::add_peer(shared_ptr<Peer> p)
{
  peers.push_back(p);
  p->start_listening(bind(&Network::handle_incoming_message, this,
                          asio::placeholders::error, p.get() ));
}

Peer:

class Peer
{
public:
  typedef function<void (const system::error_code&, Peer*)> Handler;
  void start_listening(Peer::Handler _listen_handler);
  void listen();
[...]
private:
  Handler listen_handler;
  char last_message[MESSAGE_SIZE];
[...]
};

void Peer::start_listening(Peer::Handler _listen_handler)
{
  listen_handler = _listen_handler;
  set_state(PEER_STATE_CONNECTED);
  listen();
}

void Peer::listen()
{
  memset(last_message, 0, MESSAGE_SIZE);

  socket->async_read_some(asio::buffer(last_message), listen_handler);
}

void Peer::receive()
{
    cout << get_address() << ": " << last_message << endl;
    listen();
}

G++:

g++ -c -Wall -D DEBUG_ON -Iinclude -I/usr/include src/Peer.cpp
In file included from /usr/include/boost/asio.hpp:30:0,
                 from include/Peer.hpp:4,
                 from src/Peer.cpp:3:
/usr/include/boost/asio/basic_stream_socket.hpp: In instantiation of
    ‘void boost::asio::basic_stream_socket<Protocol, StreamSocketService>::async_read_some(const MutableBufferSequence&, const ReadHandler&)
    [with
      MutableBufferSequence = boost::asio::mutable_buffers_1;
      ReadHandler = boost::function<void(const boost::system::error_code&, Peer*)>;
      Protocol = boost::asio::ip::tcp;
      StreamSocketService = boost::asio::stream_socket_service<boost::asio::ip::tcp>]’:
src/Peer.cpp:30:69:   required from here
/usr/include/boost/asio/basic_stream_socket.hpp:785:57: error: invalid conversion from ‘long unsigned int’ to ‘Peer*’ [-fpermissive]
In file included from /usr/include/boost/function/detail/maybe_include.hpp:23:0,
                 from /usr/include/boost/function/detail/function_iterate.hpp:14,
                 from /usr/include/boost/preprocessor/iteration/detail/iter/forward1.hpp:57,
                 from /usr/include/boost/function.hpp:64,
                 from include/Peer.hpp:5,
                 from src/Peer.cpp:3:
/usr/include/boost/function/function_template.hpp:754:17: error:
    initializing argument 2 of ‘boost::function2<R, T1, T2>::result_type boost::function2<R, T1, T2>::operator()(T0, T1) const
    [with
      R = void;
      T0 = const boost::system::error_code&;
      T1 = Peer*;
      boost::function2<R, T1, T2>::result_type = void]’ [-fpermissive]

I kind of understand that it's trying to use the ReadHandler constructor with (error_code, bytes_transferred).

I've read this question and it seems bind is "smart enough to not pass [stuff]", but then does that mean that there's no way to (inhales) bind a read handler to a non-static method from another class accepting other parameters than error_code, bytes_transferred?

(I tried changing the Handler typedef to void(error_code, bytes_transferred, Peer*) but it didn't work either)

Alternatively, if any of this seems to reek of bad design, I'm open to suggestions as to how to handle things better. If the problem somehow comes from not having the call to bind() inside the call to async_read(), I guess I could make Network call async_read()...

Upvotes: 0

Views: 2031

Answers (1)

Tanner Sansbury
Tanner Sansbury

Reputation: 51911

In short, change the boost::function type to only accept one argument. The bound function only has one unknown value (indicated with boost::asio::placeholders::error), as the peer is already bound:

typedef boost::function<void (const boost::system::error_code&, Peer*)> Handler;

becomes:

typedef boost::function<void (const boost::system::error_code&)> Handler;

When initializing the async_read_some operation, use boost::bind to adapt the Handler to a type that meets the requirements of Read Handler.

socket->async_read_some(asio::buffer(last_message), listen_handler);

becomes:

socket->async_read_some(asio::buffer(last_message),
  boost::bind(listen_handler, boost::asio::placeholders::error));

For further details, the error is because the boost::function type does not meet the type requirements for Boost.Asio's Read Handler. A Read Handler is required to:

  • Be CopyConstructible.
  • Accept an lvalue of type const boost::system::error_code as its first argument.
  • Accept an lvalue of type const std::size_t as its second argument.

The reason this does not surface when boost::bind is used directly is that the unspecified functor type returned from boost::bind has an operator() accepting two arguments of an unspecified type. These arguments are only forwarded to the bound function if placeholders were provided in the binding process. Otherwise, the extra arguments are silently ignored/discarded.

Thus, Boost.Asio will always call the handler with boost::system::error_code and std::size_t. In the original code, the handler type is:

boost::function<void (const boost::system::error_code&, Peer*)>

This type fails to meet the Read Handler type requirement. Boost.Asio tries to invoke the handler with the second argument as const std::size_t, resulting in the compiler error failing the conversion between std::size_t (long unsigned int) and Peer*.

invalid conversion from ‘long unsigned int’ to ‘Peer*’

By wrapping Handler with boost::bind, it adapts the Handler functor to meet the Read Handler requirements.

boost::bind(listen_handler, boost::asio::placeholders::error);

Although Boost.Asio will invoke the resulting functor with boost::system::error_code and std::size_t, the functor will invoke Handler only providing boost::system::error_code, as std::size_t will be discarded. This blog illustrates the passing and discarding of arguments.


Here is a minimal complete example:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/function.hpp>

class Peer {};

class Network
{
public:
  void handle_incoming_message(const boost::system::error_code& error,
                               Peer* peer)
  {
    std::cout << error.message() << "\n"
              << "Peer is: " << peer << std::endl;
  };
};

typedef boost::function<void (const boost::system::error_code&)> Handler;


int main()
{
  Network network;
  Peer peer;
  std::cout << "Peer is: " << &peer << std::endl;

  // Create the handler, that accepts a single argument.
  Handler handler = 
      boost::bind(&Network::handle_incoming_message, &network,
                  boost::asio::placeholders::error,
                  &peer);

  // Create io_service and socket.
  boost::asio::io_service io_service;
  boost::asio::ip::tcp::socket socket(io_service);

  // Initialize operation, adapting Handler to meet Read Handler requirements.
  socket.async_read_some(boost::asio::null_buffers(),
    boost::bind(handler,
                boost::asio::placeholders::error));

  io_service.run();
}

And the output:

Peer is: 0xbfb2a6d2
Bad file descriptor
Peer is: 0xbfb2a6d2

Upvotes: 1

Related Questions