How to use boost::outcome::result as the return type of completion token for boost::asio::co_spawn

1.2k Views Asked by At

boost::asio::co_spawn requires the completion token with the signature void(std::exception_ptr, R) for calling an awaitable function boost::asio::awaitable<R>.

When the return type of awaitable function (R) is boost::outcome::result<T>, it fails to compile, because boost::outcome::result<T> doesn't have default constructor.

auto awaitable_func() -> boost::asio::awaitable<boost::outcome::result<void>> {
  co_return std::in_place_type<void>;
}

boost::asio::co_spawn(executor, awaitable_func(), boost::asio::detached);

error: call to deleted constructor of 'boost::outcome_v2::basic_result<void, std::error_code, boost::outcome_v2::policy::error_code_throw_as_system_error<void, std::error_code, void>>'
          std::move(handler)(e, T());
                                ^
note: 'basic_result' has been explicitly marked deleted here
  basic_result() = delete;

Is there a way to use boost::outcome::result as the return type of awaitable function?

1

There are 1 best solutions below

1
On

The issue stems from co_spawns requirement that the return value must be default constructible as you found out yourself. You can workaround that requirement by using std::optional.

#include <iostream>
#include <thread>
#include <coroutine>
#include <boost/asio.hpp>
#include <boost/outcome.hpp>
#include <optional>

namespace outcome = boost::outcome_v2;
namespace asio = boost::asio;

auto awaitable_func(bool doThrow) -> asio::awaitable<outcome::result<int>> {
  if (doThrow)
    throw std::runtime_error("Issues");
  co_return outcome::success(666);
}

auto awaitable2_func(bool doThrow) -> asio::awaitable<void> {
  std::cout << "awaitable2 BEGIN doThrow: " << doThrow << std::endl;
  // no issues here calling from another coroutine
  // when an exception is thrown it just propagates it
  // the return value does not have to be default constructed
  try {
    std::cout << "awaitable2 got " << (co_await awaitable_func(doThrow)).value() << std::endl;
  } catch (std::exception &e) {
    std::cout << "What " << e.what() << std::endl;
  }
  std::cout << "awaitable2 END doThrow: " << doThrow << std::endl;
}

/**
 * You can work around the default constructible issue by using a std::optional.
 */
auto awaitable_wrapper_func(bool doThrow) -> asio::awaitable<std::optional<outcome::result<int>>> {
  co_return co_await awaitable_func(doThrow);
}

int main() {
  asio::io_context appIO;

  // Show that calling awaitable_func from another coroutine is fine
  asio::co_spawn(appIO, awaitable2_func(false), asio::detached);
  asio::co_spawn(appIO, awaitable2_func(true), asio::detached);

  // use the workaround like this
  auto fut = asio::co_spawn(appIO, awaitable_wrapper_func(false), asio::use_future);
  appIO.run();
  // need to get value twice to unpack the optional first
  std::cout << "From wrapper " << fut.get().value().value() << std::endl;

  std::cout << "Show issues" << std::endl;
  // Now the reason why co_spawn requires default constructible return values on await-ables
  for (bool it : {false, true})
    asio::co_spawn(appIO, awaitable_wrapper_func(it), [it](
      std::exception_ptr e,
      std::optional<outcome::result<int>> ret // this needs to hold some value even when the awaitable didn't return anything
    ) {
      std::cout << "handler BEGIN doThrow: " << it << std::endl;
      if (e != nullptr) {
        try {
          std::rethrow_exception(e);
        } catch (std::exception &e) {
          std::cout << "What " << e.what() << std::endl;
        }
      }

      // co_spawn had to default construct ret to support this kind of error handling
      if (ret.has_value())
        std::cout << "got val " << ret.value().value() << std::endl;
      std::cout << "handler END doThrow: " << it << std::endl;
    });
  appIO.restart();
  appIO.run();

  return 42;
}

So why does co_spawn require default constructible return values

I think this boils down to this branch. In the above example I show an alternative way of handling exceptions where the return value must be default constructed by co_spawn. Most if not all exception free handlers default construct the return value on error.