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

Unexpected memcpy on uncopyable & unmovable type when using co_await

提问人:Lukas Lang 提问时间:6/9/2023 更新时间:6/10/2023 访问量:86

问:

序言

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

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

稍后,将填充 ,并且标志将设置为 。在此之后,协程知道它可以恢复,这会导致 awaitable 在被销毁之前从缓冲区中复制结果。bufferreadytrue

可等待的对象是不可复制的,也是不可移动的,以强制到处强制删除有保证的副本,这样我就可以确保指向可等待对象的指针保持有效,直到等待完毕(至少这是计划......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()};
}

在没有任何优化的情况下使用 ARM GCC 11.3 编译此代码时,代码包含一个围绕对象移动的调用(摘自 Godbolt):memcpyAwaitableBase

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 来修改它们的行为memcpyAwaitableAwaitableBaseAwaitableBaseAwaitable
  • 没有memcpyco_await
  • 如前所述,我需要 awaitable 有一个稳定的地址,因为我依赖于这样一个事实,即我可以查看存储在 promise 中的 to 来检查 awaitable 是否已完成。ready_ptr

问题

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

C 未定义行为 复制省略 C++ 协程

评论

1赞 n. m. could be an AI 6/9/2023
看起来像这个错误,还有其他几个类似的错误(但不要引用我的话)。
0赞 Lukas Lang 6/9/2023
@n.m. 谢谢!似乎就是这样,显然我的错误搜索技能需要一些改进。我想在这种情况下,我要么希望有一些聪明的解决方法,要么重新设计整个事情......
2赞 Artyer 6/10/2023
看起来错误是直接在表达式中创建临时的。如果你把它吊起来,就像它似乎不再一样。这是否适用于您的代码库?co_awaitco_await []{ return Awaitable{make_awaitable_base()}; }();memcpy
0赞 Fedor 6/10/2023
带有正文的附加功能可以消除呼叫: godbolt.org/z/6hqxj8foP 你能把它用作解决方法吗?return Awaitable{make_awaitable_base()};memcpy
1赞 Lukas Lang 6/10/2023
@Fedor 感谢您的建议。据我所知,这也有效,不幸的是,在我的情况下应用 Artyer 建议的 lambda 有点困难。尽管如此,请记住,这是一个不错的选择

答:

3赞 Artyer 6/10/2023 #1

正如评论中指出的,这是一个 GCC 错误,其中通过在表达式中构造对象创建的 prvalues 被错误地视为可简单复制的聚合,从而创建了一个临时的 'd from.co_awaitmemcpy

解决方法是永远不要直接在表达式中构造一个重要的对象。例如,,和都容易受到此错误的影响。co_awaitco_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