10

The GCC C++ compiler (any many other C++ compilers as well) provide nonstandard extentions such as

  • alloca() for stack based allocation
  • variable length arrays, as they are part of the C standard

Can these be used inside of C++20 coroutines from a fundamental point of view? Is it possible at all? And if yes how is this implemented?

As far as I understood is that the C++20 coroutines generally create the stack-frame for the coroutine on the first call (i.e. when the promise object is created) and hence need to know the size of the coroutines stack-frame.

However this does not play nicely with alloca or other run-time dynamic stack allocation.

So is it possible and, if yes, how it is implemented? Or what are the implications?

Fedor
  • 17,146
  • 13
  • 40
  • 131
Andreas H.
  • 5,557
  • 23
  • 32
  • 1
    All the dire warnings about [`alloca`](https://man.openbsd.org/alloca.3) apply. All the problems with VLAs (even if supported as an extension) apply. And the code will be of limited portability, which doesn't matter if portability doesn't matter. – Eljay May 17 '21 at 19:57
  • "*from a fundamental point of view*" What do you mean by that? – Nicol Bolas May 17 '21 at 22:11
  • 2
    @NicolBolas I mean whether is is compatible at all, in the sense that it is implementable at all or at least in a reasonably efficient way. – Andreas H. May 18 '21 at 06:32

1 Answers1

4

Unfortunately, alloca is not compatible with C++20 coroutines in GCC. And the worst thing is that the compiler does not warn about it.

This code example demonstrates the incompatibility:

#include <coroutine>
#include <iostream>

struct ReturnObject {
  struct promise_type {
    unsigned * value_ = nullptr;

    void return_void() {}
    ReturnObject get_return_object() {
      return {
        .h_ = std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
  };

  std::coroutine_handle<promise_type> h_;
  operator auto() const { return h_; }
};

template<typename PromiseType>
struct GetPromise {
  PromiseType *p_;
  bool await_ready() { return false; }
  bool await_suspend(std::coroutine_handle<PromiseType> h) {
    p_ = &h.promise();
    return false;
  }
  PromiseType *await_resume() { return p_; }
};

ReturnObject counter()
{
  auto pp = co_await GetPromise<ReturnObject::promise_type>{};

  //unsigned a[1]; auto & i = a[0]; //this version works fine
  auto & i = *new (alloca(sizeof(unsigned))) unsigned(0); //and this not
  for (;; ++i) {
    pp->value_ = &i;
    co_await std::suspend_always{};
  }
}

int main()
{
  std::coroutine_handle<ReturnObject::promise_type> h = counter();
  auto &promise = h.promise();
  for (int i = 0; i < 5; ++i) {
    std::cout << *promise.value_ << std::endl;
    h();
  }
  h.destroy();
}

https://gcc.godbolt.org/z/8zG446Esx

It should print 0 1 2 3 4, but does not precisely due to the usage of alloca

Fedor
  • 17,146
  • 13
  • 40
  • 131
  • 2
    This is an interesting find. It could be a bug in gcc, though. – Andreas H. Jul 16 '21 at 11:40
  • 1
    When I think about it: Recursion should probabably also work with coroutines. So that has a run-time variable stack space consumption as well... – Andreas H. Jul 16 '21 at 11:43
  • 2
    I have just checked MSVC with this example, and it does not support ```alloca``` in coroutines as well. So it is either a common bug or a common limitation with the current coroutine implementation. –  Jul 20 '21 at 16:57
  • 1
    Your example looks working with gcc 12.* – Sam Pronee Oct 25 '22 at 12:15
  • 1
    @SamPronee, thanks for the observation. Indeed it works somehow with default compilation flags in GCC 12, but stops working if one adds `-O2`: https://gcc.godbolt.org/z/dnoxM91K1 – Fedor Oct 25 '22 at 13:00