提问人:Lukas Lang 提问时间:6/9/2023 更新时间:6/10/2023 访问量:86
使用不可复制和不可移动类型时出现意外的 memcpy co_await
Unexpected memcpy on uncopyable & unmovable type when using co_await
问:
序言
这是对我尝试使用代码执行的操作的描述,请跳到下一节以查看实际问题。
我想在嵌入式系统中使用协程,因为在嵌入式系统中,我负担不起太多的动态分配。因此,我正在尝试以下操作:我有不可复制、不可移动的可等待类型,用于对外围设备的各种查询。在查询外围设备时,我使用类似 .awaitable 的构造函数准备对外围设备的请求,注册其内部以接收回复,并在 promise 中注册其标志。然后挂起协程。auto result = co_await Awaitable{params}
buffer
ready
稍后,将填充 ,并且标志将设置为 。在此之后,协程知道它可以恢复,这会导致 awaitable 在被销毁之前从缓冲区中复制结果。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()};
}
在没有任何优化的情况下使用 ARM GCC 11.3 编译此代码时,代码包含一个围绕对象移动的调用(摘自 Godbolt):memcpy
AwaitableBase
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()
这破坏了我的代码,因为我依赖于无法移动/复制对象的事实。我的理解是,使一个对象不可复制和不可移动应该可以防止它被记忆复制。
意见/评论
- 在 13.1 中不再存在 - 不幸的是,我被 11.3 卡住了
memcpy
- 如果我删除 wrapped around 的 aggreate 初始化(而是使自己成为可等待的)则不存在 - 这对我不起作用,因为我想包装其他 awaitables 来修改它们的行为
memcpy
Awaitable
AwaitableBase
AwaitableBase
Awaitable
- 没有
memcpy
co_await
- 如前所述,我需要 awaitable 有一个稳定的地址,因为我依赖于这样一个事实,即我可以查看存储在 promise 中的 to 来检查 awaitable 是否已完成。
ready_ptr
问题
我该如何解决这个问题?
是编译器的错误,还是我对保证复制省略的误解?依赖临时地址在呼叫期间不应更改这一事实是否属于未定义的行为?co_await
答:
正如评论中指出的,这是一个 GCC 错误,其中通过在表达式中构造对象创建的 prvalues 被错误地视为可简单复制的聚合,从而创建了一个临时的 'd from.co_await
memcpy
解决方法是永远不要直接在表达式中构造一个重要的对象。例如,,和都容易受到此错误的影响。co_await
co_await Class{ ... }
co_await function_call(Class{ ... })
co_await Class{ ... }.member_function()
您可以将它们替换为(即 ,其中 lambda 类型可以被 memcpy 复制)co_await [&]{ return ...; }();
co_await lambda_type(captured_references...)()
您可能希望将其宏观化,以便只需在代码库中搜索小写即可完全消除此错误。#define CO_AWAIT(...) co_await [&]() -> decltype(auto) { return __VA_ARGS__ ; }()
co_await
评论
co_await
co_await []{ return Awaitable{make_awaitable_base()}; }();
memcpy
return Awaitable{make_awaitable_base()};
memcpy