Search code examples
c++c++11boost-asiosmart-pointersasio

Strange behavior when passing a cable captures `std::initializer_list<std::shared_ptr<Conversation>>` to the complete handler


The code snippet below always close the connection at once when a client has connected to the server, which is really out of my expection.

#include <iostream>
#include <memory>
#include <system_error>
#include "tcp_server.hpp"


TcpServer::TcpServer(asio::io_context& io_context,
      const unsigned int& port):m_acceptor(io_context, tcp::endpoint(tcp::v4(), port))
{
  do_accept();
}

void TcpServer::do_accept()
{
      m_acceptor.async_accept(make_strand(m_acceptor.get_executor()),
      [this](const std::error_code& error, tcp::socket socket)
      {
            if (!error)
            {
              std::make_shared<Conversation>(std::move(socket))->do_start();
            }
            else
            {
                std::cerr << "Error accepting connection: " << error.message() << std::endl;
            }

            // Start accepting the next connection
            do_accept();
      });
}

Conversation::Conversation(tcp::socket socket) : m_socket(std::move(socket))
{ 
  std::cout << "creating conversation:" << this << std::endl;
}

void Conversation::do_read()
{
    std::cout << "do read" << std::endl;
    auto conversation_ptr{shared_from_this()};
     m_socket.async_read_some(asio::buffer(m_data),
        [this, conversation_ptr](std::error_code error, size_t length) {
          if (!error)
          {
              do_write(std::string("reply:") + std::string{m_data.data(), length});
              do_read();
              std::cout << "data read: " << m_data.data() << std::endl;
          }
          else
          {
              std::cout << "error in reading data: " << error.message() << std::endl;
          }
        });
}

void Conversation::do_start()
{
  do_read();
}

void Conversation::do_write(std::string str)
{
  auto conversation_ptr{shared_from_this()};
  m_socket.async_send(asio::buffer(str), [this, conversation_ptr](std::error_code error, size_t length ){
    if (!error)
    {
        std::cout << "data write successfully" << std::endl;
    }
    else
    {
        std::cout << "error in writing data: " << error.message() << std::endl;
    }
  });
}

Conversation::~Conversation()
{
   std::cout << "destroying conversation:" << this << std::endl;
}

int main()
{
  asio::io_context io_context;
  TcpServer server{io_context, 12345};

  io_context.run();
}

Since auto conversation_ptr{shared_from_this()}; constructs an instance of std::initializer_list<std::shared_ptr<Conversation>> and the complete handler captures the conversation_ptr, I think the instance of Conversation would be kept alive when the complete hander is invoked if some message is sent from the client.However the output of the server always something like this below.

./tcp_server 
creating conversation:0x1cad8d0
do read
destroying conversation:0x1cad8d0
error in reading data: Operation aborted.

^C

At the mean time, here is the output of telnet below, which idicates the server other than the client closes the connection.

telnet 0.0.0.0 12345
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
Connection closed by foreign host.

What's more, it works well if the auto conversation_ptr{shared_from_this()}; is replaced by auto conversation_ptr(shared_from_this());. What a surprise!

The used compiler is clang++ 3.4.

Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)
Target: x86_64-pc-linux-gnu
Thread model: posix

The used asio library is V1.29.0.

#define ASIO_VERSION 102900 // 1.29.0

The platform is ubuntu14.04.

Distributor ID: Ubuntu
Description:    Ubuntu 14.04.6 LTS
Release:        14.04
Codename:       trusty

NOTE: As per this post, the definition of max_align_t has been added to make the code snippet compile with clang++ 3.4.


Solution

  • std::initializer_list holds a reference to a temporary array. This is lifetime extended like a normal const reference, i.e., the temporary ends its lifetime when the original initializer_list ends its lifetime.

    So, the captured copy initializer_list becomes dangling once the lambda is called: the shared_ptr it was referring to has already been destroyed. The reason auto conversation_ptr(shared_from_this()); works is that you capture a shared_ptr directly and the lambda holds a copy.


    Also auto conversation_ptr{shared_from_this()}; only makes a std::initializer_list<std::shared_ptr<Conversation>> before N3922. Afterwards, it becomes a std::shared_ptr<Conversation>. Starting from Clang 3.6, you started getting this warning:

    warning: direct list initialization of a variable with a deduced type will change meaning in a future version of Clang; insert an '=' to avoid a change in behavior [-Wfuture-compat]
    

    And was finally implemented in Clang 3.8, so I suspect this code was written for a newer compiler