2

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
MHebes
  • 2,290
  • 1
  • 16
  • 29
  • 1
    Is `#include ` needed? What happens without it? – Ted Lyngmo Jul 05 '23 at 19:04
  • Without it the compiler complains `Please define _WIN32_WINNT or _WIN32_WINDOWS appropriately... Assuming _WIN32_WINNT=0x0601 (i.e. Windows 7 target).` and I get the same output. https://stackoverflow.com/questions/10539391/what-is-sdkddkver-h-for https://stackoverflow.com/a/16288859/3554391 – MHebes Jul 05 '23 at 19:43

1 Answers1

3

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Nice! Would `std::reference_wrapper`s help here just like when starting threads? – Ted Lyngmo Jul 05 '23 at 19:50
  • Gotcha. I suppose the `string` example does make sense to me—the temporary `string` dies immediately after the function call, and so by the time the coroutine is resumed initially, it already has a dangling reference (which manifests itself as an "empty string" in this case). Just need some clarification on the `char[12]&` version: when you say "But that is a reference to a *temporary* array", you're saying that it's actually going to copy the static string from the `.data` section into a temporary array, then give a reference to the temporary rather than the original string? – MHebes Jul 05 '23 at 20:14
  • 1
    "reference to a temporary array" - I don't think so? String literals have static storage duration, there's no reference to temporary here. If MSVC is dying here, that seems like an MSVC bug to me. – Barry Jul 05 '23 at 22:08