Luk*_*ang 7 c++ undefined-behavior copy-elision c++-coroutine
这是我尝试使用代码执行的操作的描述,请跳到下一部分以查看实际问题。
我想在嵌入式系统中使用协程,但我无法承受太多的动态分配。因此,我正在尝试以下操作:对于对外围设备的各种查询,我有不可复制、不可移动的可等待类型。查询外围设备时,我使用类似auto result = co_await Awaitable{params}. 可等待的构造函数准备对外围设备的请求,注册其内部buffer以接收回复,并ready在承诺中注册其标志。然后协程被挂起。
稍后,buffer将被填充,并且ready标志将被设置为true。此后,协程知道它可以恢复,这会导致等待对象在被销毁之前从缓冲区复制出结果。
可等待的内容是不可复制且不可移动的,以强制在任何地方进行有保证的复制省略,这样我就可以确保指向buffer并ready保持有效的指针,直到等待可等待的内容为止(至少这是计划......)
我在以下代码中遇到 ARM GCC 11.3 的问题:
#include <cstring>
#include <coroutine>
struct AwaitableBase {
AwaitableBase() = default;
AwaitableBase(const AwaitableBase&) = delete;
AwaitableBase(AwaitableBase&&) = delete;
AwaitableBase& operator=(const AwaitableBase&) = delete;
AwaitableBase& operator=(AwaitableBase&&) = delete;
char buffer[65];
};
struct task {
struct promise_type
{
bool* ready_ptr;
task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
struct Awaitable{
AwaitableBase base;
bool ready{false};
bool await_ready() {return false;}
void await_suspend(std::coroutine_handle<task::promise_type> handle)
{
handle.promise().ready_ptr = &ready;
}
int await_resume() { return 2; }
};
AwaitableBase make_awaitable_base()
{
return AwaitableBase{};
}
task example()
{
co_await Awaitable{make_awaitable_base()};
}
Run Code Online (Sandbox Code Playgroud)
当使用 ARM GCC 11.3 编译此代码而不进行任何优化时,代码包含一个在对象memcpy周围移动的调用AwaitableBase(摘自Godbolt):
ldr r3, [r7, #4]
adds r3, r3, #87
mov r0, r3
bl make_awaitable_base()
ldr r2, [r7, #4]
ldr r3, [r7, #4]
add r0, r2, #21
adds r3, r3, #87
movs r2, #65
mov r1, r3
bl memcpy
ldr r3, [r7, #4]
movs r2, #0
strb r2, [r3, #86]
ldr r3, [r7, #4]
adds r3, r3, #21
mov r0, r3
bl Awaitable::await_ready()
Run Code Online (Sandbox Code Playgroud)
这破坏了我的代码,因为我依赖于对象无法移动/复制的事实。我的理解是,使对象不可复制和不可移动应该可以防止它被内存复制。
memcpy存在 - 不幸的是,我被困在 11.3 中memcpy的聚合初始化(而是使其自身成为可等待的),则不存在 -这对我不起作用,因为我想包装其他可等待的以修改它们的行为AwaitableAwaitableBaseAwaitableBaseAwaitablememcpy没有the则不存在co_awaitready_ptrPromise 中存储的地址来检查等待项是否已完成。我该如何解决这个问题?这是编译器的错误,还是我误解了保证复制省略的某些内容?依赖于临时地址在调用期间不应更改的事实是否是未定义的行为co_await?
正如评论中指出的,这是一个GCC 错误,其中通过在表达式中构造对象创建的纯右值co_await被错误地视为可简单复制的聚合,memcpy从而创建了一个临时值。
解决方法是永远不要直接在表达式中构造非平凡对象co_await。例如,co_await Class{ ... }、co_await function_call(Class{ ... })和co_await Class{ ... }.member_function()都容易出现此错误。
您可以将它们替换为co_await [&]{ return ...; }();(即co_await lambda_type(captured_references...)(),其中 lambda 类型可以被 memcpy 复制)
您可能希望对其进行宏化,以便您只需在代码库中#define CO_AWAIT(...) co_await [&]() -> decltype(auto) { return __VA_ARGS__ ; }()搜索小写字母即可完全消除此错误。co_await