4

I'd like to create an asynchronous function which takes as it's last argument boost::asio::yield_context. E.g.:

int async_meaning_of_life(asio::yield_context yield);

I'd also like to be consistent with how Asio returns error codes. That is, if the user does:

int result = async_meaning_of_life(yield);

and the function fails, then it throws the system_error exception. But if the user does:

boost::error_code ec;
int result = async_meaning_of_life(yield[ec]);

Then - instead of throwing - the error is returned in ec.

The problem is that when implementing the function, I can't seem to find a clean way to check whether the operator[] was used or not and set it if so. We came up with something like this:

inline void set_error(asio::yield_context yield, sys::error_code ec)
{
    if (!yield.ec_) throw system_error(ec);
    *(yield.ec_) = ec;
}

But that's hacky, because yield_context::ec_ is declared private (although only in the documentation).

One other way I can think of doing this is to convert the yield object into asio::handler_type and execute it. But this solution seems awkward at best.

Is there another way?

Peter Jankuliak
  • 3,464
  • 1
  • 29
  • 40

1 Answers1

4

Asio uses async_result to transparently provide use_future, yield_context or completion handlers in its API interfaces.¹

Here's how the pattern goes:

template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
    typename asio::handler_type<Token, void(error_code, int)>::type
                 handler (std::forward<Token> (token));

    asio::async_result<decltype (handler)> result (handler);

    if (success)
        handler(42);
    else
        handler(asio::error::operation_aborted, 0);

    return result.get ();
}

Update

Starting with Boost 1.66 the pattern adheres to the interface proposed for standardization:

    using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
    typename result_type::completion_handler_type handler(std::forward<Token>(token));

    result_type result(handler);

Comprehensive Demo

Showing how to use it with with

  • coro's and yield[ec]
  • coro's and yield + exceptions
  • std::future
  • completion handlers

Live On Coliru

#define BOOST_COROUTINES_NO_DEPRECATION_WARNING 
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/use_future.hpp>

using boost::system::error_code;
namespace asio = boost::asio;

template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
#if BOOST_VERSION >= 106600
    using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
    typename result_type::completion_handler_type handler(std::forward<Token>(token));

    result_type result(handler);
#else
    typename asio::handler_type<Token, void(error_code, int)>::type
                 handler(std::forward<Token>(token));

    asio::async_result<decltype (handler)> result (handler);
#endif

    if (success)
        handler(error_code{}, 42);
    else
        handler(asio::error::operation_aborted, 0);

    return result.get ();
}

void using_yield_ec(asio::yield_context yield) {
    for (bool success : { true, false }) {
        boost::system::error_code ec;
        auto answer = async_meaning_of_life(success, yield[ec]);
        std::cout << __FUNCTION__ << ": Result: " << ec.message() << "\n";
        std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
    }
}

void using_yield_catch(asio::yield_context yield) {
    for (bool success : { true, false }) 
    try {
        auto answer = async_meaning_of_life(success, yield);
        std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
    } catch(boost::system::system_error const& e) {
        std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
    }
}

void using_future() {
    for (bool success : { true, false }) 
    try {
        auto answer = async_meaning_of_life(success, asio::use_future);
        std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";
    } catch(boost::system::system_error const& e) {
        std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
    }
}

void using_handler() {
    for (bool success : { true, false })
        async_meaning_of_life(success, [](error_code ec, int answer) {
            std::cout << "using_handler: Result: " << ec.message() << "\n";
            std::cout << "using_handler: Answer: " << answer << "\n";
        });
}

int main() {
    asio::io_service svc;

    spawn(svc, using_yield_ec);
    spawn(svc, using_yield_catch);
    std::thread work([] {
            using_future();
            using_handler();
        });

    svc.run();
    work.join();
}

Prints:

using_yield_ec: Result: Success
using_yield_ec: Answer: 42
using_yield_ec: Result: Operation canceled
using_yield_ec: Answer: 0
using_future: Answer: 42
using_yield_catch: Answer: 42
using_yield_catch: Caught: Operation canceled
using_future: Caught: Operation canceled
using_handler: Result: Success
using_handler: Answer: 42
using_handler: Result: Operation canceled
using_handler: Answer: 0

Note: for simplicity I have not added output synchronization, so the output can become intermingled depending on runtime execution order


¹ see e.g. this excellent demonstration of how to use it to extend the library with your own async result pattern boost::asio with boost::unique_future

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thanks @sehe, but this is the "convert the `yield` object into `asio::handler`" solution which I mentioned in the question. I'm not trying to create a generic async function which supports handlers, futures and coroutines. Quite opposite, I'm trying to stick with coroutines for as long as possible for code clarity. – Peter Jankuliak Oct 22 '17 at 08:01
  • Consider this answer as confirmation of your fears then? What you want to do is going to stay awkward until someone comes along with new information (or the library API gets extended). – sehe Oct 22 '17 at 10:29
  • Sorry for the delay. I think if you modify your answer to explicitly mention somewhere at the beginning that it is currently not possible I'll be happy to mark it as an answer until someone proves us wrong. – Peter Jankuliak Oct 26 '17 at 14:27
  • This does not work in in boost 1.66, gives a compilation error. – Amin Roosta Jan 28 '18 at 13:43
  • @AminRoosta Yes. You need to indicate you want to keep using the deprecated interfaces - since [1.66 introduced breaking changes](http://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/history.html) to anticipate standardized executors [as proposed](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4656.pdf). The [list of changes](http://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/net_ts.html) includes it. – sehe Jan 28 '18 at 17:45
  • @AminRoosta Updated the answer with the new interface as well (had to glance at [this well hidden doc page](http://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/reference/asynchronous_operations/completion_token.html) to get it together) – sehe Jan 28 '18 at 18:32
  • 1
    @sehe Awesome, Thank you so much. you made my day ;-) – Amin Roosta Jan 29 '18 at 13:23