使用 co_await 时,不可复制和不可移动类型上出现意外的 memcpy

Luk*_*ang 7 c++ undefined-behavior copy-elision c++-coroutine

前言

这是我尝试使用代码执行的操作的描述,请跳到下一部分以查看实际问题。

我想在嵌入式系统中使用协程,但我无法承受太多的动态分配。因此,我正在尝试以下操作:对于对外围设备的各种查询,我有不可复制、不可移动的可等待类型。查询外围设备时,我使用类似auto result = co_await Awaitable{params}. 可等待的构造函数准备对外围设备的请求,注册其内部buffer以接收回复,并ready在承诺中注册其标志。然后协程被挂起。

稍后,buffer将被填充,并且ready标志将被设置为true。此后,协程知道它可以恢复,这会导致等待对象在被销毁之前从缓冲区复制出结果。

可等待的内容是不可复制且不可移动的,以强制在任何地方进行有保证的复制省略,这样我就可以确保指向bufferready保持有效的指针,直到等待可等待的内容为止(至少这是计划......)

问题

我在以下代码中遇到 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)

这破坏了我的代码,因为我依赖于对象无法移动/复制的事实。我的理解是,使对象不可复制和不可移动应该可以防止它被内存复制。

观察/评论

  • 13.1 中不再memcpy存在 - 不幸的是,我被困在 11.3 中
  • 如果我删除了包装memcpy的聚合初始化(而是使其自身成为可等待的),则不存在 -这对我不起作用,因为我想包装其他可等待的以修改它们的行为AwaitableAwaitableBaseAwaitableBaseAwaitable
  • memcpy没有the则不存在co_await
  • 如前所述,我需要等待项有一个稳定的地址,因为我依赖这样一个事实:我可以查看ready_ptrPromise 中存储的地址来检查等待项是否已完成。

问题)

我该如何解决这个问题?这是编译器的错误,还是我误解了保证复制省略的某些内容?依赖于临时地址在调用期间不应更改的事实是否是未定义的行为co_await

Art*_*yer 3

正如评论中指出的,这是一个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