Home > OS >  C second invocation of eagerly evaluated coroutine ovewrites first output
C second invocation of eagerly evaluated coroutine ovewrites first output

Time:03-29

I have a following code where I want to pass lambda or any other invocable object into an awaiter to call it at await_suspend(). Using co_await while using void(int) works properly, however, when I try to get a return value from a returning coroutine using co_return by using eager evaluation, the program does not behave as expected, however while using lazy evaluation, everything works correctly. However, both attempts cause Invalid read by size 4. AFAIK everything is done by the documentation (https://en.cppreference.com/w/cpp/language/coroutines).

#include <iostream>
#include <coroutine>
#include <tuple>
#include <functional>

#define DEBUG(x) std::cout << __FILE__ << ": " << __FUNCTION__ << ": " << __LINE__ << ": " << x << std::endl

// set lazy evaluation by uncommentting this line
//#define LAZY

/* create nonvoid task that will return value from coroutine through the promise_type */
template <typename RET>
struct nonvoid_task {
  struct promise_type;
  using handle_type = std::coroutine_handle<promise_type>;

  handle_type coro;

  /* coroutine with handler */
  nonvoid_task(handle_type cr) : coro(cr) {
      DEBUG("Creating non void coroutine");
  }

  /* destructor, no destroyign of handler here */
  ~nonvoid_task() {
      DEBUG("Destroying coroutine");
  }

  /* get return value from the coroutine handler */
  RET get() {
      DEBUG("Return value");
      DEBUG(std::hex << &(coro.promise().result) << std::dec);
#ifdef LAZY
      coro.resume();
#endif
      return coro.promise().result;
  }

  /* promise type which is used to return the value using co_return */
  struct promise_type {
    RET result;

    /* get return object */
    auto get_return_object() {
        auto task = nonvoid_task{handle_type::from_promise(*this)};
        DEBUG("get_return_object(): " << std::hex << &task << std::dec);
        return task;
    }

    /* set initial suspend to either lazy or eager eval */
#ifdef LAZY
    std::suspend_always initial_suspend() {
#else
    std::suspend_never initial_suspend() {
#endif
        DEBUG("");
        return {};
    }

    std::suspend_never final_suspend() noexcept {
        DEBUG("");
        return {};
    }

    /* get return value to the member result */
    void return_value(RET v) {
        DEBUG("Setting return value " << std::hex << &result << std::dec << " to " << v);
        result = v;
    }

    void unhandled_exception() {}
  };
};

/* template returning nonvoid_task that takes separate arguments */
template <typename RET, typename F, typename ... ARGS>
nonvoid_task<RET> ret_test(F to_invoke, ARGS ... args) {
    DEBUG("Not tuple");
    co_return std::invoke(to_invoke, args...);
}

int main() {
    /* function returning int for nonvoid_task */
    auto ret_func = [] (int a, int b) { return a * b; };

    /* invoke coroutines through overloaded templates */
    auto A = ret_test<int>(ret_func, 42, 2);
    auto B = ret_test<int>(ret_func, 42, 3);

    /* get value from coroutine A and B and print it */
    std::cout << A.get() << std::endl;
    std::cout << B.get() << std::endl;

    return 0;
}

Not only that coroutine A and B print 126 and 126, instead of 84 and 126, they even share same address of coro.promise().result, which should not happen according to documentation. This does not happen when using lazy evaluation.

Valgrind output:

==36893== Memcheck, a memory error detector
==36893== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==36893== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==36893== Command: ./a.out
==36893==
test.cpp: nonvoid_task: 21: Creating non void coroutine
test.cpp: get_return_object: 46: get_return_object(): 0x1ffefff5b0
test.cpp: initial_suspend: 56:
test.cpp: ret_test: 78: Not tuple
test.cpp: return_value: 67: Setting return value 0x5b3ac90 to 84
test.cpp: final_suspend: 61:
test.cpp: nonvoid_task: 21: Creating non void coroutine
test.cpp: get_return_object: 46: get_return_object(): 0x1ffefff5a8
test.cpp: initial_suspend: 56:
test.cpp: ret_test: 78: Not tuple
test.cpp: return_value: 67: Setting return value 0x5b3ad00 to 126
test.cpp: final_suspend: 61:
test.cpp: get: 31: Return value
test.cpp: get: 32: 0x5b3ac90
==36893== Invalid read of size 4
==36893==    at 0x401576: nonvoid_task<int>::get() (test.cpp:36)
==36893==    by 0x400BC9: main (test.cpp:91)
==36893==  Address 0x5b3ac90 is 16 bytes inside a block of size 48 free'd
==36893==    at 0x4C2DB0C: operator delete(void*) (vg_replace_malloc.c:802)
==36893==    by 0x400F73: ret_test(nonvoid_task<int> ret_test<int, main::{lambda(int, int)#1}, int, int>(main::{lambda(int, int)#1}, int, int)::_Z8ret_testIiZ4mainEUliiE_JiiEE12nonvoid_taskIT_ET0_DpT1_.frame*) [clone .actor] (test.cpp:80)
==36893==    by 0x400CF1: nonvoid_task<int> ret_test<int, main::{lambda(int, int)#1}, int, int>(main::{lambda(int, int)#1}, int, int) (test.cpp:77)
==36893==    by 0x400BA7: main (test.cpp:87)
==36893==  Block was alloc'd at
==36893==    at 0x4C2B751: operator new(unsigned long) (vg_replace_malloc.c:417)
==36893==    by 0x400C89: nonvoid_task<int> ret_test<int, main::{lambda(int, int)#1}, int, int>(main::{lambda(int, int)#1}, int, int) (test.cpp:80)
==36893==    by 0x400BA7: main (test.cpp:87)
==36893==
84
test.cpp: get: 31: Return value
test.cpp: get: 32: 0x5b3ad00
==36893== Invalid read of size 4
==36893==    at 0x401576: nonvoid_task<int>::get() (test.cpp:36)
==36893==    by 0x400BEE: main (test.cpp:92)
==36893==  Address 0x5b3ad00 is 16 bytes inside a block of size 48 free'd
==36893==    at 0x4C2DB0C: operator delete(void*) (vg_replace_malloc.c:802)
==36893==    by 0x400F73: ret_test(nonvoid_task<int> ret_test<int, main::{lambda(int, int)#1}, int, int>(main::{lambda(int, int)#1}, int, int)::_Z8ret_testIiZ4mainEUliiE_JiiEE12nonvoid_taskIT_ET0_DpT1_.frame*) [clone .actor] (test.cpp:80)
==36893==    by 0x400CF1: nonvoid_task<int> ret_test<int, main::{lambda(int, int)#1}, int, int>(main::{lambda(int, int)#1}, int, int) (test.cpp:77)
==36893==    by 0x400BBD: main (test.cpp:88)
==36893==  Block was alloc'd at
==36893==    at 0x4C2B751: operator new(unsigned long) (vg_replace_malloc.c:417)
==36893==    by 0x400C89: nonvoid_task<int> ret_test<int, main::{lambda(int, int)#1}, int, int>(main::{lambda(int, int)#1}, int, int) (test.cpp:80)
==36893==    by 0x400BBD: main (test.cpp:88)
==36893==
126
test.cpp: ~nonvoid_task: 26: Destroying coroutine
test.cpp: ~nonvoid_task: 26: Destroying coroutine
==36893==
==36893== HEAP SUMMARY:
==36893==     in use at exit: 0 bytes in 0 blocks
==36893==   total heap usage: 3 allocs, 3 frees, 72,800 bytes allocated
==36893==
==36893== All heap blocks were freed -- no leaks are possible
==36893==
==36893== For lists of detected and suppressed errors, rerun with: -s
==36893== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Could you please tell me what am I doing wrong?

CodePudding user response:

Your problem is here:

std::suspend_never final_suspend() noexcept

A coroutine basically looks like this:

Type actual_coroutine(args)
{
  promise p;
  initial-suspend-point;
  { /*Body of coroutine*/ }
  final-suspend-point;
}

"Body of coroutine" is the body of code you put into your coroutine, where p is the promise object for your coroutine.

As you can see, the final suspend point happens outside of the coroutine body. But it also happens in the same scope as p.

But note that there's nothing after the final suspend point. So, what happens when a function runs out of stuff to do?

It ends, returning control to the caller. But also, all of the stack objects in its scope are destroyed.

Objects like p; your promise. So by the time you call get, the promise object it tries to talk to has been destroyed.

Oops.

As such, if you want to access your promise object after the coroutine effectively ends (and in your case, you do), you must not use suspend_never. You should use suspend_always instead.

Not suspending at the final suspend point is mainly for things like generators, which may not need the promise after they flow off the end of the coroutine.

  • Related