Search code examples
c++asynchronousboostobject-lifetimeasio

boost::asio data owning `ConstBufferSequence`


The last days, I read alot through the asio examples and other questions here on SO regarding lifetime management of buffers passed to asios initiating functions. One issue that strikes me, is that there seems to be no "by value" solution, but instead the caller of the initiating function always needs to ensure that the buffers are valid until the async operation completes. Often this is achieved by creating shared pointers to the data and copying them into the handlers.

In my case I am already using the async_write overload where a ConstBufferSequence is passed in which holds e.g. the sizeof the next message followed by the next message. I.e.

auto message = std::make_shared<std::string>("hello");
auto size = std::make_shared<std::uint32_t>(message.size());
std::vector<asio::const_buffer> buffers = {asio::buffer(size, sizeof(*size)), asio::buffer(*message)};
asio::async_write(_socket, buffers, [message,size](...){...}); // prolong lifetime by coping shared ptrs.

So I was thinking about writing a custom Message class that fulfills the ConstBufferSequence concept but also owns the underlying message. I dug a little into the code, and found, that at one place the buffer sequence argument, which is at first passed into asio::async_write via const& gets passed along via const& until it finally is copied into a member variable of the asio::detail::write_op class.

So here the actual question(s):

  • Can this approach work for a fire and forget call? This would translate above intos something like: asio::async_write(_socket, Message("hello"),[](auto,auto){});

  • Are there any issues with the lifetime of the constbuffer sequence arguments w.r.t. composed operations?

  • Would this be a good idea? It obviously goes against the "normal" asio way of dealing with buffer lifetime management.

Curious about your thoughts - Criticism welcome! ;-)

Regards, Martin


Solution

  • I've seen a shared_buffer before (I think it was in the Asio docs/exmples, will search later).

    Furthermore there's obviously

    • boost::asio::streambuf
    • the Beast buffer models (flat_buffer, multi_buffer, vector_buffer)

    Of course, it just moves the problem from an ownership issue into a life-time issue.

    The upside of having Boost Asio's buffer concept being "ref-only" or "view-semantics" is that

    • you never run into life-time issues passing those references around by value , and
    • they lend themselves very well to implementing composed operations the right way: without assumptions about the buffer organization

    Found the Asio example: https://www.boost.org/doc/libs/1_42_0/doc/html/boost_asio/example/buffers/reference_counted.cpp these days lives in

    • example/cpp03/buffers/reference_counted.cpp
    • example/cpp11/buffers/reference_counted.cpp

    Addressing the bullets

    Can this approach work for a fire and forget call? This would translate above intos something like: asio::async_write(_socket, Message("hello"),{});

    Yes

    Are there any issues with the lifetime of the constbuffer sequence arguments w.r.t. composed operations?

    Not if you manage it well, which is a requirement anyways

    Would this be a good idea? It obviously goes against the "normal" asio way of dealing with buffer lifetime management.

    Depends on how you do it. If you end up copying the buffer wholesale it would be costly, and also wouldn't work with some async operations (which require the buffer to have reference stability, meaning: not change address, in other words, not move around).

    Granted with move-semantics many buffer types might still be able to offer this property, but the problem with buffers being passed by-value is that they might make multiple copies, which

    • either doesn't compile (because they're not rvalues)
    • or (if you hack it with a moving copy constructor/assignment) might lead to UB when a moved-from copy is used