Actually, yes, this is possible, but you must be careful to ensure that the lifetime of the coroutine ends before returning from the function that called alloca. For simple synchronous generator coroutines that you fully use up in the same function, that's easy enough.
The trick is to take advantage of the fact that when you provide a custom operator new on the coroutine's promise type, the compiler tries to pass in all the parameters the coroutine received. You can designate a parameter as a special allocation parameter and use it to communicate with the allocation code in operator new. The compiler provides the required size to both operator new and operator delete, and if operator new returns nullptr then the compiler uses get_return_object_on_allocation_failure to initialize the return value.
Putting this all together, we can make a coroutine promise type that allows invoking the coroutine in a two-step approach, the first time to get the required buffer size, and the second time to actually allocate into the buffer, similar to many C APIs. Here's a basic example which allows the allocation parameter to be nullptr when dynamic allocation is preferred, so you can choose which uses of the coroutine should use alloca or not:
#include <coroutine>
#include <cstddef>
#include <memory>
#include <new>
#include <print>
#include <span>
#include <utility>
#if defined(_WIN32)
#include <malloc.h>
#define alloca _alloca
#else
#include <alloca.h>
#endif
struct AllocationParam
{
std::span<std::byte> buffer{};
std::size_t used{};
};
template<typename T>
class Generator
{
public:
class promise_type;
private:
using handle_type = std::coroutine_handle<promise_type>;
handle_type handle{};
public:
class promise_type
{
T* value{};
friend Generator;
public:
constexpr promise_type() noexcept = default;
promise_type(promise_type const&) = delete;
promise_type(promise_type&&) = delete;
promise_type& operator=(promise_type const&) = delete;
promise_type& operator=(promise_type&&) = delete;
[[nodiscard]] handle_type get_return_object() noexcept
{
return handle_type::from_promise(*this);
}
[[nodiscard]] static constexpr handle_type get_return_object_on_allocation_failure() noexcept
{
return {};
}
[[nodiscard]] static void* operator new(std::size_t const length, auto&&... params) noexcept
{
AllocationParam* param{};
(([&]() noexcept //this lambda is run for each of params to find the one we want
{
if constexpr(std::is_same_v<AllocationParam*, std::remove_cvref_t<decltype(params)>>)
{
param = params;
}
}()), ...);
if(param)
{
param->used = length + 1;
if(std::size(param->buffer) >= param->used)
{
param->buffer[length] = std::byte{0}; //un-set this byte to note that we didn't dynamically allocate
return std::data(param->buffer);
}
return nullptr;
}
std::byte* const memory{static_cast<std::byte*>(::operator new(length + 1, std::nothrow))};
if(memory)
{
memory[length] = std::byte{1}; //set this byte to 1 to note that we dynamically allocated
}
return memory;
}
static void operator delete(void* const ptr, std::size_t const length) noexcept
{
std::byte* const memory{static_cast<std::byte*>(ptr)};
if(memory[length] == std::byte{1})
{ //this memory was dynamically allocated
return ::operator delete(memory, length + 1);
}
}
[[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { return {}; }
[[nodiscard]] constexpr std::suspend_always final_suspend() const noexcept { return {}; }
void unhandled_exception() const { throw; }
[[nodiscard]] constexpr std::suspend_always yield_value(T&& t) noexcept
{
value = std::addressof(t);
return {};
}
constexpr void return_void() const noexcept {}
};
constexpr Generator() noexcept = default;
constexpr Generator(handle_type const h) noexcept
: handle{h}
{
}
Generator(Generator const&) = delete;
Generator& operator=(Generator const&) = delete;
constexpr Generator(Generator&& f) noexcept
{
using std::swap;
swap(handle, f.handle);
}
constexpr Generator& operator=(Generator&& f) noexcept
{
using std::swap;
swap(handle, f.handle);
return *this;
}
~Generator() noexcept
{
if(handle)
{
handle.destroy();
}
}
[[nodiscard]] constexpr operator bool() const noexcept
{
return handle && !handle.done();
}
[[nodiscard]] T* operator()()
{
if(handle && !handle.done())
{
handle.resume();
return std::exchange(handle.promise().value, nullptr);
}
return nullptr;
}
};
[[nodiscard]] Generator<int> example(AllocationParam* const = nullptr)
{
co_yield 1;
co_yield 2;
co_yield 2;
co_yield 5;
}
int main()
{
AllocationParam param{};
Generator<int> g(example(¶m));
if(g)
{
std::println("coroutine allocation was elided");
}
else
{
std::println("coroutine allocation requested {} bytes", param.used);
void* const memory{alloca(param.used)};
param.buffer = {static_cast<std::byte*>(memory), param.used};
param.used = 0;
g = example(¶m);
if(g && param.used == 0)
{
std::println("coroutine allocation was elided after alloca");
}
if(!g)
{
std::println("coroutine allocation failed, requested {} bytes", param.used);
}
}
while(int* const v{g()})
{
std::println("generator yielded: {}", *v);
}
}
Notice that each time we call the coroutine function (example in this case) there is a potential for the compiler to decide to elide the allocation, or to request a different amount of memory each time. In practice I have not seen this happen, but it's worth being aware that it's a possibility just in case. In my tests with MSVC, debug builds request 369 bytes and release builds request 81 bytes, so there's a noticable bloat in coroutine frame size with debug builds especially, even with simple coroutines such as this.
Note that use of alloca is not required here, another possibility with this code is that you can re-use memory across multiple coroutines this way, as long as the coroutine lifetimes do not overlap. You can allocate that memory by any means you like - it could be a static or thread local global buffer, or it can be a dynamically allocated buffer that you keep resizing until it no longer receives resize requests, etc.
newto allocate the coroutine in the space you allocate withalloca()?sizeofandalignof, no?