Search code examples
c++visual-c++c++20boost-asioc++-coroutine

Asio coroutines behaving poorly with rvalue parameters?


I might be doing something obviously wrong, but why would this print garbage on MSVC?

#include <sdkddkver.h>

#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;

template <typename Str>
asio::awaitable<void> coro_task(Str&& str) {
  std::cout << "coro_task: " << str << std::endl;
  co_await asio::this_coro::executor;
}

template <typename Str>
void sync_task(Str&& str) {
  std::cout << "sync_task: " << str << std::endl;
}

int main() {
  asio::io_context ioc;
  asio::co_spawn(ioc.get_executor(), coro_task("hello world"), asio::detached);
  asio::post(ioc.get_executor(), [] { sync_task("hello world"); });

  ioc.run();
}

This gives for example:

coro_task: ╕÷ⁿäg
sync_task: hello world

I can't figure out why this is the case. Surely Str would be a static-lifetime char[12] &, so I can't figure out why its getting garbage data.

If I pass anything with a temporary lifetime to coro_task (e.g. coro_task(std::string{"hello world"})) I get no output (str is empty). I assume it's being moved from somewhere, but don't know where.

This issue was encountered while trying to create a generic async task/corouitine wrapper for blocking functions: https://stackoverflow.com/a/75228866/3554391. This question represents a minimal example of the issue.

The issue does not appear to happen with gcc on godbolt or with clang.

I am using visual studio 17.5.5 and asio 1.25.0.


Solution

  • Broadly speaking, coroutines should not take parameters by reference. Unlike, for example, std::async, zero effort is made by C++'s coroutine system to try to turn by-reference parameters into local copies.

    This explains its behavior when you're giving it objects like std::string. To understand its behavior when dealing with string literals directly... things become complex.

    In a normal function, parameters are effectively created by the calling function. Since a coroutine must outlive the context of the cite of its function call, it has a mechanism to "copy" parameters into storage associated with the coroutine state. If the parameter is a value parameter, then it will be initialized by an xvalue expression from the originating parameter (and therefore, can be moved from). However, if those parameters are reference parameters, the "copy" will also be a reference of the same type.

    And this is where we run into a bit of an issue. Template argument deduction for forwarding references of string literals yields a reference to a string array. But that is a reference to a temporary array, manifested by using a string literal as a parameter to the template function. If that function had taken a char const*, then the array in question would be a pointer to static memory. But instead, what you have is a reference to a temporary, which is owned by the caller of the function.

    And therefore will expire the minute the coroutine suspends.