Currently, the draft at:
<
http://cplusplus.github.io/networking-ts/draft.pdf>
prescribes that stream read algorithms operating on DynamicBuffer objects take the buffer by a forwarding reference. For example:
template<class SyncReadStream, class DynamicBuffer>
size_t read(SyncReadStream& stream, DynamicBuffer&& b);
read(stream, dynamic_buffer(s));
The problem with this interface can be seen for the case where a user simply wants to read into their own instance of dynamic buffer:
beast::multi_buffer b; // meets the requirements of DynamicBuffer
read(stream, b);
This code will compile but do the wrong thing. The implementation takes ownership of b by move construction, leaving the caller with a moved-from object. After the read operation completes, the received data will be discarded when the scope of the newly move constructed object is exited. Clearly this is not the intention of the algorithm.
This code will also compile, and produce undesirable results:
beast::multi_buffer b;
read(stream, std::move(b));
There are two ways to make algorithms like read() work with dynamic buffers. The first is to provide special overloads for particular dynamic buffers. For example, asio::basic_streambuf meets the dynamic buffer requirements (actually it is the object that inspired the DynamicBuffer concept). But basic_streambuf cannot work with read algorithms which receive the DynamicBuffer by forwarding reference. For this reason, the net-ts compatible version of Asio provides overloads of read() to plug the hole created by this design flaw:
<
https://github.com/chriskohlhoff/asio/blob/230c0d2ae035c5ce1292233fcab03cea0d341264/asio/include/asio/read.hpp#L414>
The other solution is to create a "dynamic buffer wrapper" which meets the requirements of DynamicBuffer and forwards its member function calls to an external object. In other words a non-owning reference. Example:
template<class DynamicBuffer>
struct dynamic_buffer_wrapper;
template<class DynamicBuffer>
dynamic_buffer_wrapper<DynamicBuffer>
make_dynamic_buffer_wrapper(DynamicBuffer& buffer);
With these declarations a user can wrap their dynamic buffer at each call site to achieve the correct results:
beast::multi_buffer b;
read(stream, make_dynamic_buffer_wrapper(b));
This is not a novel idea, the net-ts compatible version of Asio creates such a wrapper internally, called basic_streambuf_ref to plug the hole created by this design flaw:
<
https://github.com/chriskohlhoff/asio/blob/230c0d2ae035c5ce1292233fcab03cea0d341264/asio/include/asio/basic_streambuf.hpp#L362>
The wrapper approach is unsatisfying because it imposes an unnecessary notational burden at every call site, with little gain.
There is a larger problem lurking. Without explicitly stating it, the Networking-TS specification and accompanying reference implementation has managed to create two categories of dynamic buffer. One is the set of dynamic buffers which own their storage, and the other is the set of dynamic buffer wrappers which simply reference storage elsewhere and can be moved and destroyed without releasing the referenced storage.
The specification and implementation require that all dynamic buffers passed to net-ts algorithms are of the second variety. That is, that they do not own their storage. As such, they are fundamentally incompatible with existing practice such as the wide variety of dynamic buffers found in Boost.Beast:
Table 1.5 Dynamic Buffer Implementations
<
http://www.boost.org/doc/libs/master/libs/beast/doc/html/beast/using_io/buffer_types.html>
There is a simple solution to this problem. That is to change the signature of Net-TS algorithms which receive dynamic buffer parameters to use regular non-const references. The signature for read, shown earlier, would be modified thusly:
template<class SyncReadStream, class DynamicBuffer>
size_t read(SyncReadStream& stream, DynamicBuffer& b);
With this signature, we can eliminate the MoveConstructible requirement from DynamicBuffer. However, callers who wish to use the dynamic_string_buffer and dynamic_vector_buffer non-owning wrappers would be slightly inconvenienced. They can no longer construct the wrapper directly in the argument list, since temporaries cannot bind to non-const references. Instead they must explicitly declare the wrapper, like this:
std::string s;
dynamic_string_buffer<std::string> b;
read(socket, b);
For the asynchronous case, the caller will have to manage the lifetime of the dynamic buffer wrapper. Some may feel that this is an unnecessary burden. I speculate that the wrappers were written to "make the networking-ts more approachable for novices." I feel that this is not a valid justification given the problems that it introduces, for two reasons.
First, in the synchronous case the caller already has the stack available. The transient wrapper can be placed there.
Second, for the asynchronous case, the caller already has to manage the lifetime of the wrapped object such as the string or vector. Requiring that they also manage the lifetime of the transient dynamic buffer wrapper is not a significant inconvenience.
My goal in making this post is to inform the networking-ts stakeholders of the problem. It is causing particular distress for me because I am trying to port Beast (<
https://github.com/boostorg/beast>) to net-ts Asio which will appear in Boost 1.66.0 and it changes all of the function signatures. It is also unsightly for users - I have to now explain to them the difference between the two types of dynamic buffers (owning and non-owning) because the Net-TS specification does not explain it. And the users must be informed of the need to wrap their dynamic buffers at every single call site.
I would like to hear from anyone with some knowledge about net-ts, and especially the folks who participated in the meetings which led to these changes. I will also try to write a paper to address it for the next meeting.
Thanks