【协程】C++是如何通过SFINAE来检查promise_type的

我们知道在C++20的协程中,自己实现的Coroutine中必须包含一个Promise,并且这个Promise必须要实现:

  • get_return_object()
  • initial_suspend()
  • final_suspend()
  • unhandled_exception()

少了其中任何一个,编译器都会报错。那这是怎么实现的呢?如果是像java那样是一个接口而没有对应的实现从而报错还能理解,但是我们的代码中的Promise完完全全是我们自己写的,也没有使用继承,编译器你怎么知道我在实现协程时少了什么东西呢?

答案就是:SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)。

SFINAE 主要应用于函数模板的重载解析过程中。当编译器尝试选择一个合适的函数模板重载版本时,如果某个模板参数的替换导致编译错误,编译器不会立即报错,而是继续尝试其他可能的重载版本。

在 C++20 协程中,编译器会根据协程函数的定义和 promise_type 的实现来生成协程的执行代码。promise_type 是一个用户定义的类型,它必须提供一些特定的函数,如 get_return_objectinitial_suspendfinal_suspendunhandled_exception 和 return_value 等。这些函数定义了协程的行为和状态,编译器会在编译期检查这些函数的存在性和正确性,以确保协程能够正确地执行。

编译器使用 SFINAE 机制来检查 promise_type 中是否包含特定的函数。具体来说,就是编译器会在协程的实现代码中使用表达式 SFINAE,尝试调用这些函数,并根据调用的结果来确定函数的存在性。

我们来写个简单的例子,只检查get_return_object函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <type_traits>

/*
* check(int) 是一个重载的模板函数,用于检查U类型是否有get_return_object()函数。
* decltype(std::declval<U>().get_return_object(), std::true_type{}) 会在U具有get_return_object()时返回std::true_type。
* 如果U没有get_return_object(),则会匹配check(...)重载,返回std::false_type。
* value成员是一个常量布尔值,指示T是否具有get_return_object()函数。
*/
template <typename T>
struct has_get_return_object {
template <typename U>
static auto check(int) -> decltype(std::declval<U>().get_return_object(), std::true_type{});

template <typename>
static std::false_type check(...);

static constexpr bool value = decltype(check<T>(0))::value;
};

template <typename T>
void check_promise_type() {
static_assert(has_get_return_object<T>::value, "Promise type must have a get_return_object() method.");
}

struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() {
return {};
}
};
};

int main() {
check_promise_type<MyCoroutine::promise_type>();
}

当我们去掉Mycoroutine::promise_type中的get_return_object函数时,check_promise_type函数中就会出发断言失败,从而编译不通过。

这也就是在将一个函数变为协程时编译器是如何检查的一个大概思路了。不得不感叹,C++真是博大精深。