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

Dav*_*ger 21 c++ c++20 c++-coroutine

背景

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


简要问题

具有以下测试功能:

Task<int> test_yielding()
{
    co_yield 1;
    co_return 2;
}
Run Code Online (Sandbox Code Playgroud)

从 Task 对象中检索了两个值。

auto a = co_await fn;
auto b = co_await fn;
Run Code Online (Sandbox Code Playgroud)

a 的值预期为 1,b 的值预期为 2。

结果针对 进行测试a + b == 3

上面的测试通过了,但是下面的测试失败了:

auto res = co_await fn + co_await fn
Run Code Online (Sandbox Code Playgroud)

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

在 MSVC 和 GCC 中,代码失败,因为它们似乎重新排序await_resumereturn_value并且yield_value.


细节

我已经通过 clang tidy、PVS studio 运行了代码,启用了 LLVM、GCC、MSVC 中所有可用的清理程序,并且没有任何相关的弹出窗口(只是关于销毁和恢复的评论不是 noexcept)。

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

功能:

Task<int> test_yielding()
{
    co_yield 1;
    co_return 2;
}
Run Code Online (Sandbox Code Playgroud)

测试 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);
Run Code Online (Sandbox Code Playgroud)

测试 2(失败):

Title("Test co_yield + co_return rvalue");
auto fn = test_yielding();
auto res =
(
    co_await fn +
    co_await fn
);
ASSERT(res == 3);
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

如果您查看工作 MSVC FAIL 和 MSVC PASS 之间的差异(地址已更正,则会出现以下内容): 在此处输入图片说明 这清楚地表明以下几行已重新排序:

        AwaitAwaitable: await_resume: 02901E20  
        GetValue: 02901E20
Run Code Online (Sandbox Code Playgroud)

LLVM 和 GCC 的源代码和结果在这里

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

差异中突出显示的行是由以下来源产生的:

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();
    }
};
Run Code Online (Sandbox Code Playgroud)

有谁知道这里发生了什么,这是编译器/库/用户错误吗?

Lew*_*ker 8

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

在这种情况下,GCC 和 MSVC 似乎await_resume()co_await在从第二个挂起点恢复之后(即在执行添加之前)错误地对两个表达式的调用进行了排序。

相反,他们应该在从第一个挂起点恢复之后和开始计算第二个表达式之前立即await_resume()对第一个co_await表达式(不确定是哪个)的调用进行排序co_await

  • [expr.await] p5.2 节指出: &gt; 如果await-ready 的结果为true,或者当协程恢复时,将对await-resume 表达式求值,其结果是await-表达式的结果。对我来说,这意味着当协程恢复时,将立即评估等待表达式。但是,如果我们根据 [intro.execution] p10 来阅读它,它表示除非另有指定,否则子表达式是不排序的,并且 [expr.add] 没有为内置运算符+参数指定任何排序,那么编译器可能会将 co_await 部分视为未排序...? (3认同)