C++20 协程,await_resume、return_value 和 yield_value 的意外重新排序

C++20 Coroutines, Unexpected reordering of await_resume, return_value and yield_value

背景

我有一个既可以 co_return 又可以 co_yield 的任务类型。 在 LLVM 中,任务按预期工作并通过了一些早期测试。在 MSVC 和 GCC 中,代码以同样的方式失败(巧合?)。


小问题

具有以下测试功能:

Task<int> test_yielding()
{
    co_yield 1;
    co_return 2;
}

从任务对象中检索到两个值。

auto a = co_await fn;
auto b = co_await fn;

a的值应该是1,b的值应该是2。

结果针对 a + b == 3.

进行了测试

以上测试通过,但以下测试失败:

auto res = co_await fn + co_await fn

GCC 和 MSVC 的 res 值为 4。两者都是从最终 co_return 中检索到的。据我了解,co_await fn 的第一次和第二次调用应该是 1 和 2 的任意顺序。

在 MSVC 和 GCC 中,代码失败,因为它们似乎重新排序 await_resumereturn_valueyield_value


详情

我有 运行 通过 clang tidy、PVS studio 的代码,在 LLVM、GCC、MSVC 中启用了所有可用的消毒程序,但没有弹出任何相关内容(只是关于 destroy 和 resume 不是 noexcept 的评论)。

我有几个非常相似的测试: 相关测试是:

函数:

Task<int> test_yielding()
{
    co_yield 1;
    co_return 2;
}

测试 1(通过):

Title("Test co_yield + co_return lvalue");
auto fn = test_yielding();
auto a = co_await fn;
auto b = co_await fn;
ASSERT(a + b == 3);

测试 2(失败):

Title("Test co_yield + co_return rvalue");
auto fn = test_yielding();
auto res =
(
    co_await fn +
    co_await fn
);
ASSERT(res == 3);

测试MSVC 1(PASS)的结果:

---------------------------------
Title   Test co_yield + co_return lvalue
---------------------------------
        get_return_object: 02F01DA0
        initial_suspend: 02F01DA0
        await_transform: 02D03C80
        AwaitAwaitable: await_ready: 02F01DA0
        AwaitAwaitable: await_suspend: 02F01DA0
        SetCurrent: 02F01DA0
        ContinueWith: 02F01DA0
        yield_value: 02F01DA0
        SetValue: 02F01DA0
        YieldAwaitable: await_ready: 02F01DA0
        YieldAwaitable: await_suspend: 02F01DA0
        ContinueWith: 02F01DA0
        AwaitAwaitable: await_resume: 02F01DA0
        GetValue: 02F01DA0
        await_transform: 02D03C80
        AwaitAwaitable: await_ready: 02F01DA0
        AwaitAwaitable: await_suspend: 02F01DA0
        SetCurrent: 02F01DA0
        ContinueWith: 02F01DA0
        YieldAwaitable: await_resume: 02F01DA0
        return_value: 02F01DA0
        SetValue: 02F01DA0
        final_suspend: 02F01DA0
        YieldAwaitable: await_ready: 02F01DA0
        YieldAwaitable: await_suspend: 02F01DA0
        ContinueWith: 02F01DA0
        AwaitAwaitable: await_resume: 02F01DA0
        GetValue: 02F01DA0
PASS    test_task:323 a + b == 3
        [ result = 3, expected = 3 ]
        Destroy: 02F01DA0

测试MSVC 2 (FAIL)的结果:

---------------------------------
Title   Test co_yield + co_return rvalue
---------------------------------
        get_return_object: 02F01CA0
        initial_suspend: 02F01CA0
        await_transform: 02D03C80
        AwaitAwaitable: await_ready: 02F01CA0
        AwaitAwaitable: await_suspend: 02F01CA0
        SetCurrent: 02F01CA0
        ContinueWith: 02F01CA0
        yield_value: 02F01CA0
        SetValue: 02F01CA0
        YieldAwaitable: await_ready: 02F01CA0
        YieldAwaitable: await_suspend: 02F01CA0
        ContinueWith: 02F01CA0
        await_transform: 02D03C80
        AwaitAwaitable: await_ready: 02F01CA0
        AwaitAwaitable: await_suspend: 02F01CA0
        SetCurrent: 02F01CA0
        ContinueWith: 02F01CA0
        YieldAwaitable: await_resume: 02F01CA0
        return_value: 02F01CA0
        SetValue: 02F01CA0
        final_suspend: 02F01CA0
        YieldAwaitable: await_ready: 02F01CA0
        YieldAwaitable: await_suspend: 02F01CA0
        ContinueWith: 02F01CA0
        AwaitAwaitable: await_resume: 02F01CA0
        GetValue: 02F01CA0
        AwaitAwaitable: await_resume: 02F01CA0
        GetValue: 02F01CA0
FAIL    test_task:342 res == 3
        [ result = 4, expected = 3 ]
        Destroy: 02F01CA0

如果您查看正在运行的 MSVC FAIL 和 MSVC PASS 之间的差异(地址已更正,将出现以下内容): 这清楚地表明以下行已重新排序:

        AwaitAwaitable: await_resume: 02901E20  
        GetValue: 02901E20

LLVM 和 GCC 的来源和结果是 here

查看 GCC FAIL 和 LLVM PASS 之间的测试 2 差异: GCC 中发生了非常相似的重新排序。

diff 中突出显示的行由以下来源生成:

template <typename Promise>
struct AwaitAwaitable
{
    Promise & m_promise;

    bool await_ready() const noexcept
    {
        WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
        return false;
    }

    void await_suspend(default_handle handle) noexcept
    {
        WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
        m_promise.SetCurrent( m_promise.Handle() );
        m_promise.ContinueWith( handle );
    }

    auto await_resume() const noexcept
    {
        WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
        return m_promise.GetValue();
    }
};

有人知道这是怎么回事吗,这是 compiler/library/user 错误吗?

观察到的行为似乎是由于 GCC 和 MSVC 在处理 addition-operator 时存在类似的错误,其中参数都是 co_await 表达式。

在这种情况下,GCC 和 MSVC 似乎在从第二个 suspend-point 恢复后(即就在执行加法)。

他们应该在从第一个 suspend-point 恢复之后和开始评估之前立即对第一个 co_await 表达式(不确定是哪个)的 await_resume() 调用进行排序第二个 co_await 表达式。