Visual Studio 2022中C++20协程局部变量致栈溢出问题咨询
C++20协程栈溢出问题分析
在Visual Studio 2022(C++20编译环境)中,运行以下代码会触发栈溢出异常(operator new从未被调用):
struct cCoro { struct promise_type { cCoro get_return_object() { return { // 使用C++20指定初始化语法 .Handle = std::coroutine_handle<promise_type>::from_promise(*this) }; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() {} void return_void() {} void* operator new (size_t sz) { printf("%s %I64d\n", __FUNCTION__, sz); return malloc(sz); } }; std::coroutine_handle<promise_type> Handle; }; cCoro stack_or_heap() { const int sz = 64 * 1024 * 1024; // 远大于普通线程栈大小,触发栈溢出 uint8_t buffer[sz]; co_await std::suspend_always(); // ... 此处使用buffer } int main() { auto h = stack_or_heap(); // <== 此处触发栈溢出 }
将buffer大小修改为以下值时,代码可正常运行(且operator new会被调用):
cCoro stack_or_heap() { const int sz = 64 * 1024; // 运行正常 uint8_t buffer[sz]; co_await std::suspend_always(); // ... 此处使用buffer }
针对上述现象,解答以下三个疑惑:
1. C++协程中的“局部”变量是存储在栈上还是堆上?
C++协程中的局部变量(包括示例中的buffer)最终会存储在协程状态对象中,而协程状态对象默认通过堆分配(operator new)创建。但在协程初始化阶段,编译器可能先在当前调用栈上临时分配空间,用于构造协程状态,之后再将其转移到堆上(如果需要持久化挂起状态)。
2. 无论buffer大小如何,我预期其分配都会通过operator new完成,为何会出现栈溢出?
这是Visual Studio编译器的协程栈分配优化策略导致的:当协程局部变量总大小较小时,编译器会先尝试在当前调用栈上完整分配协程状态所需空间,再决定是否移到堆上;当变量总大小超过某个阈值(你的环境中约为64KB),编译器会直接使用堆分配。
第一个例子中,buffer大小为64MB,远超线程栈默认大小(通常1MB或8MB),编译器在栈上尝试预留空间时直接触发溢出,根本没走到堆分配逻辑,所以operator new从未被调用。
3. 查看Godbolt反汇编代码,进入协程时会在栈上预留buffer大小的空间,同时又通过operator new为协程状态(包含buffer)分配空间,为何会为同一buffer同时在栈和堆上分配空间?
这是编译器的临时栈分配+拷贝迁移机制:
- 编译器先在调用栈上为协程状态(包括局部变量)分配临时空间,用于初始化promise对象、局部变量等;
- 由于你的代码中
initial_suspend返回suspend_always,协程会立即挂起,此时编译器会将栈上的协程状态完整拷贝到operator new分配的堆空间中; - 后续协程的挂起、恢复操作都基于堆上的状态执行,栈上的临时空间会在协程初始化函数返回时被释放。
当局部变量过大时,栈上的临时分配直接触发溢出,导致后续堆分配逻辑无法执行。
内容的提问来源于stack exchange,提问作者Dr James Cain
相关产品推荐
相关产品推荐

