2

I was looking at one page of the tutorial of boost::asio.

class tcp_server
{
public:

  tcp_server(boost::asio::io_service& io_service)
    : acceptor_(io_service, tcp::endpoint(tcp::v4(), 13))
  {
    start_accept();
  }

private:

  void start_accept()
  {
    tcp_connection::pointer new_connection =
      tcp_connection::create(acceptor_.get_io_service()); // shared_ptr got created.
    acceptor_.async_accept(new_connection->socket(),
      boost::bind(&tcp_server::handle_accept, this, new_connection,
      boost::asio::placeholders::error)); // instance added to io_service task list, but bind does not use shared_ptr internally I believe.
  } // shared_ptr of tcp_connection goes out of scope.

  void handle_accept(tcp_connection::pointer new_connection,
  const boost::system::error_code& error)
  {
    if (!error)
    {
      new_connection->start();
    }
    start_accept();
  }

class tcp_connection
  : public boost::enable_shared_from_this<tcp_connection>
{
public:
  typedef boost::shared_ptr<tcp_connection> pointer;

  static pointer create(boost::asio::io_service& io_service)
  {
    return pointer(new tcp_connection(io_service));
  }

  tcp::socket& socket()
  {
    return socket_;
  }

  void start()
  {
    message_ = make_daytime_string();
    boost::asio::async_write(socket_, boost::asio::buffer(message_),
      boost::bind(&tcp_connection::handle_write, shared_from_this(),
      boost::asio::placeholders::error,
      boost::asio::placeholders::bytes_transferred));
  }

private:
  tcp_connection(boost::asio::io_service& io_service)
    : socket_(io_service)
  {
  }

  void handle_write(const boost::system::error_code& /*error*/,
      size_t /*bytes_transferred*/)
  {
  }

  tcp::socket socket_;
  std::string message_;
};

and I found that there's one portion of run time that no shared_ptr object of tcp_connection object is alive. That seems to mean that the tcp_connection object would get destroyed at the beginning of that portion since the count in its shared_ptr goes down to zero, which is obviously not what we want.

But then I saw the comment in the class tcp_connection quote

We will use shared_ptr and enable_shared_from_this because we want to keep the tcp_connection object alive as long as there is an operation that refers to it.

And I also did a search for this problem, and got a Q&A in SO here. But I am still puzzled by the titled question. Specifically what does it mean there is an operation that refers to it? At the time when tcp_server::start_accept() returns, all shared_ptr of tcp_connection instances should go out of scope, and probably only some raw pointer reference was added to the io_service task list. How does enabled_shared_from_this prevent the heap instance of tcp_connection get destroyed when there's no instances of the shared_ptr of this tcp_connection object? Or it has nothing to do with enabled_shared_from_this, but the boost::asio::io_service keeps shared_ptr of the bounded async_handler internally?

Community
  • 1
  • 1
WiSaGaN
  • 46,887
  • 10
  • 54
  • 88

2 Answers2

3

The object is being kept alive by the functor.

Notice how the handle_accept callback function takes a shared_ptr to tcp_connection as an argument. The magic now happens inside the boost::bind call that is used for specifying the callback to async_accept.

That bind call returns a functor object that stores all of the arguments needed to invoke handle_accept by value. So the functor returned by bind contains a copy of the shared_ptr to tcp_connection and thus keeps it alive. The functor (think of it as a boost::function) is now in turn copied by the async_accept to allow io_service to perform the callback once the accept operation completes.

So the chain of ownership is: io_service owns the functor, which owns the shared_ptr which owns the tcp_connection. The same thing also works without enable_shared_from_this by the way.

ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
1

This question might be answered from describing the nonintrusive design of shared_ptr, which means not touching "any" code of its pointed object.

We can get a lot of pros from its nonintrusive design. But on other hand, it leads to the situation inevitably: once shared_ptr, shared_ptr everywhere without raw pointer.

Let's see this snippet:

{
    shared_ptr<int> orig_sp{new int};
    shared_ptr<int> good_sp{orig_sp};       // it's ok
    shared_ptr<int> bad_sp{orig_sp.get()};  // leads crashing
}

This snippet would make your program crash because of invoking deleting twice for the same raw pointer(new int). The root cause is that orig_sp shares reference count with good_sp while doesn't do it with bad_sp. That means you cannot create a shared_ptr from a raw pointer which has already be shared_ptred.

Then go back to the ASIO example code. It's already well known to make the life cycle of new_connection longer than tcp_server::start_accept()'s. In this case, shared_ptr is a very useful template to prolong the life cycle accurately.

void handler(shared_ptr<vector<char>> buffer, /* other declarations */)
{
    // works after write.
    // buffer will be deleted automatically.
}

int main()
{
    shared_ptr<vector<char>> buffer{new vector<char>};

    // preparing buffer

    boost::asio::async_write(socket_, boost::asio::buffer(*buffer),
      boost::bind(handler, buffer, boost::asio::placeholders::error,
        boost::asio::placeholders::bytes_transferred));
}

But the problem is that a member function of a shared_ptred object wants to prolong its own life cycle.

class tcp_connection {
    void start()
    {
        boost::asio::async_write(socket_, boost::asio::buffer(message_),
          boost::bind(&tcp_connection::handle_write, (/* how to fill this field? */),
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    }
};
  • Filling this in that field is unacceptable because it will not prolong tcp_connection's life cycle.
  • Filling boost::shared_ptr<tcp_connection>(this) is not acceptable too. It will cause a crash according to our first snippet.
  • Perhaps boost::shared_ptr<tcp_connection>(tcp_connection::create(acceptor_.get_io_service())) looks good, but it's trying to create object itself in its member.

Now it's very clear. tcp_connection::start() wants to know which shared_ptr managed, but according to nonintrusive design of shared_ptr, tcp_connection shouldn't contain any information for shared_ptr. It's mission impossible.

At last, we have to compromise, and ask enable_shared_from_this for help. enable_shared_from_this requires that class must be derived from it by using CRTP idiom. That's why the tcp_connection looks so tricky.

class tcp_connection : boost::enable_shared_from_this<tcp_connection> {
    void start()
    {
        // ...
        boost::asio::async_write(socket_, boost::asio::buffer(message_),
          boost::bind(&tcp_connection::handle_write, shared_from_this(),
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    }
};

enable_shared_from_this template keeps some fields like reference count for shared_ptr, and provides shared_from_this for ones whose member functions want to use shared_ptr instead of this.

Matt Yang
  • 661
  • 4
  • 8
  • Thanks for the reply. But it doesn't seem to answer the question. The problem is if boost::asio::io_service is not using `shared_ptr` to store the handler and its parameters, then all `shared_ptr` would get destroyed by the end. So it probably depends on the implementation of the asio::io_service handler implementation. – WiSaGaN Oct 16 '13 at 08:01
  • 1
    `boost::asio::io_service` uses functors to keep callback which is the return value of `boost::bind` in this case. And the functor stores `shared_ptr`. When deleting functor, shared_ptr` will be deleted too. – Matt Yang Oct 16 '13 at 08:06
  • 1
    Ok, I know your real requirement now. Sorry for the boring answer. I think [this post](http://stackoverflow.com/questions/4871273/passing-rvalues-through-stdbind/4871563#4871563) might be fit to you. – Matt Yang Oct 16 '13 at 08:11
  • Yeah, that post helps. Thanks! – WiSaGaN Oct 16 '13 at 08:14
  • "you cannot create a shared_ptr from a raw pointer which has already be shared_ptred." -- false. You cannot create two `shared_ptr` to the same raw pointer where both want to `delete` the pointer. The problem is two `ptr`s thinking they can `delete`, not two `ptr`s. If you have a different destruction code, having two `shared_ptr` to the same raw pointer is fine. This can be a useful pattern when you have an underlying reference counted object, and you want to create a smart reference counted pointer to it, and you don't want to write or use your own intrusive smart pointer class. – Yakk - Adam Nevraumont Oct 16 '13 at 15:14
  • Yes, your comment is much more accurate. Mine is just fit to the default shared_ptr destructor. – Matt Yang Oct 17 '13 at 01:12