逻辑题:如何利用 C++20 的协程特性实现一个无限长度的斐波那契数列生成器?

各位编程爱好者,大家好!

今天,我们将深入探讨 C++20 引入的一项革命性特性:协程(Coroutines)。我们将利用这项强大的工具,解决一个经典而又富有挑战性的问题——如何实现一个无限长度的斐波那契数列生成器。在传统的编程范式中,生成一个“无限”序列通常意味着要么耗尽内存,要么需要复杂的迭代器管理和状态保存。而 C++20 协程,以其独特的挂起(suspend)和恢复(resume)能力,为我们提供了一种优雅而高效的解决方案。

本讲座将从斐波那契数列的基础开始,逐步引入协程的核心概念,然后详细阐述如何构建一个通用的协程生成器类型,最终将其应用于实现我们无限长度的斐波那契数列。我们将深入代码细节,探讨设计选择,并讨论相关的性能、内存和错误处理考量。

斐波那契数列:一个经典的序列

斐波那契数列是一个在数学、自然界和计算机科学中广泛出现的数列。它的定义非常简单:

  1. 数列的前两个数字是 0 和 1(或 1 和 1,取决于定义,我们采用 0, 1 的标准)。
  2. 从第三个数字开始,每个数字都是前两个数字之和。

因此,斐波那契数列通常以以下形式开始:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …

其数学表达式为:
$F_0 = 0$
$F_1 = 1$
$Fn = F{n-1} + F_{n-2}$,对于 $n ge 2$

在编程中,我们通常可以通过递归或迭代的方式来生成斐波那契数列。

递归实现示例(效率低下,仅作演示):

long long fibonacci_recursive(int n) {
    if (n <= 0) return 0;
    if (n == 1) return 1;
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);
}

递归实现虽然直观,但由于重复计算,效率极低,不适合生成长序列。

迭代实现示例(更常用):

#include <vector>
#include <iostream>

std::vector<long long> generate_fibonacci_iterative(int count) {
    if (count <= 0) return {};
    if (count == 1) return {0};

    std::vector<long long> fib_series;
    fib_series.push_back(0);
    fib_series.push_back(1);

    for (int i = 2; i < count; ++i) {
        fib_series.push_back(fib_series[i - 1] + fib_series[i - 2]);
    }
    return fib_series;
}

// int main() {
//     std::vector<long long> fib = generate_fibonacci_iterative(10);
//     for (long long n : fib) {
//         std::cout << n << " ";
//     }
//     std::cout << std::endl; // Output: 0 1 1 2 3 5 8 13 21 34
//     return 0;
// }

迭代实现效率更高,但它有一个根本的限制:它需要预先知道要生成多少个数字,并将所有生成的数字存储在一个 std::vector 中。这意味着,如果我们要生成一个“无限”序列,或者一个非常长的序列(例如,数万亿个数字),这种方法将迅速耗尽系统内存。

我们真正需要的是一个“生成器”(Generator)模式:一个能够按需(on-demand)生成序列下一个元素的机制,而不是一次性生成所有元素。每次请求时,它计算并返回下一个值,同时保留其内部状态,以便下次请求时能从上次中断的地方继续。这正是 C++20 协程所擅长的领域。

C++20 协程:异步与生成器的基石

C++20 协程是一项语言级别的特性,它允许函数在执行过程中暂停(suspend)并在稍后从暂停点恢复(resume)。这与传统的函数调用不同,传统函数一旦返回,其局部状态就会被销毁。协程则能在暂停时保留其栈帧(coroutine frame),包括局部变量和执行点,以便后续恢复。

协程并非多线程并发模型。它们是“协作式”的,意味着一个协程只有在明确地“挂起”自己时,控制权才会交回给调用者或调度器。它们在单个线程内运行,避免了多线程中的锁和竞争条件等复杂性。

C++20 协程引入了三个新的关键字:

  • co_await:用于等待一个“可等待对象”(awaitable object)完成。通常用于异步操作,如网络请求、文件I/O等。当 co_await 一个可等待对象时,协程可能会挂起,直到可等待对象准备好结果。
  • co_yield:用于从协程中产生(yield)一个值给调用者,并挂起协程。这是实现生成器模式的关键。
  • co_return:用于从协程中返回一个值(或不返回值),并结束协程的执行。

除了这三个关键字,协程的实现还需要一些底层机制,这些机制通常由标准库或第三方库提供。这些机制围绕着一个核心概念:promise_type

协程的生命周期简述:

  1. 当一个标记为协程的函数(即函数体中包含 co_await, co_yield, 或 co_return 之一)被调用时,编译器会将其转换为一个特殊的结构。
  2. 它不会立即执行函数体,而是首先创建一个协程帧(coroutine frame)来存储局部变量、参数和执行状态。
  3. 它会构造一个 promise_type 对象。promise_type 是协程与外部世界交互的桥梁,它定义了协程的行为,例如在初始挂起/恢复、产生值、结束时如何操作。
  4. promise_type::get_return_object() 方法被调用,返回一个“协程句柄”(coroutine handle)或一个封装了协程句柄的对象(例如我们的 generator 类型)。这个对象是调用者与协程交互的唯一途径。
  5. 根据 promise_type::initial_suspend() 的返回值,协程可能会立即挂起(默认行为,也是生成器模式所期望的),或者立即开始执行。
  6. 当调用者通过协程句柄请求恢复协程时,协程从上次挂起的地方继续执行。
  7. 当协程执行到 co_yield 语句时,它将产生一个值,并再次挂起,将控制权交回给调用者。
  8. 当协程执行到 co_return 语句或到达函数体末尾时,它将结束执行。promise_type::final_suspend() 会被调用,决定协程在结束时是否挂起。
  9. 协程帧的销毁由协程句柄负责,当句柄被销毁时,协程帧才会被清理。

构建一个通用的协程生成器 generator<T>

为了实现斐波那契数列生成器,我们首先需要一个通用的 generator<T> 类型。这个类型将作为协程的返回类型,并封装了与协程交互的逻辑,使其能像标准库迭代器一样被使用(例如在范围for循环中)。

一个 generator<T> 类型至少需要以下几个关键组件:

  1. generator<T> 类本身: 封装了 std::coroutine_handle,提供迭代器接口。
  2. promise_type 协程内部状态和行为的定义。
  3. iterator 类: 使得 generator<T> 能够支持范围for循环。

让我们逐步构建这些组件。

1. generator<T>::promise_type

promise_type 是协程机制的核心,它定义了协程的行为。对于一个生成器,promise_type 必须提供以下方法:

| 方法名称 | 返回类型/参数 | 作用

  • get_return_object():由 std::coroutine_traits 调用,用于获取与协程句柄相关的对象。
  • initial_suspend():当协程首次被调用时,它会首先执行 initial_suspend()。如果返回 std::suspend_always,协程会立即挂起,等待 resume() 调用。如果返回 std::suspend_never,协程会立即开始执行。对于生成器,我们通常希望立即挂起,以便调用者可以显式地开始迭代。
  • final_suspend():当协程即将结束(co_return 或函数体末尾)时,会调用此方法。如果返回 std::suspend_always,协程会在结束时挂起,允许调用者检查最终状态或从其结果中提取值。如果返回 std::suspend_never,协程会立即销毁其协程帧。对于生成器,通常在结束时挂起,以便调用者可以检查结束状态。
  • yield_value(T value):当协程执行到 co_yield value 时,此方法会被调用。它通常将 value 存储在 promise_type 的成员变量中,并返回 std::suspend_always 以挂起协程。
  • return_void():当协程执行到 co_return; 或到达函数体末尾时,此方法会被调用。对于不返回最终值的生成器,这是必需的。
  • unhandled_exception():如果协程内部抛出未捕获的异常,此方法会被调用。在这里,我们可以处理异常,例如重新抛出异常,或者将其存储起来供调用者检查。

考虑到 generator<T> 的通用性,我们还需要一个 std::coroutine_handle<promise_type> 来管理协程实例。

#include <coroutine> // C++20 standard header for coroutines
#include <exception> // For std::current_exception, std::rethrow_exception
#include <optional>  // For std::optional to hold the yielded value
#include <stdexcept> // For std::logic_error

// 前向声明 generator 类,因为 promise_type 需要知道它
template <typename T>
struct generator;

// generator<T> 的 promise_type 定义
template <typename T>
struct generator_promise {
    T value_; // 存储 co_yield 产生的值
    std::exception_ptr exception_; // 存储协程内部的异常

    // get_return_object:当协程被调用时,返回一个 generator 对象
    // 这个 generator 对象会包装 coroutine_handle
    generator<T> get_return_object();

    // initial_suspend:协程首次被调用时立即挂起
    // 这允许调用者在第一次迭代前获得 generator 对象
    std::suspend_always initial_suspend() const noexcept { return {}; }

    // final_suspend:协程结束时挂起
    // 这允许调用者在协程结束后检查其状态(例如,是否有异常)
    std::suspend_always final_suspend() const noexcept { return {}; }

    // yield_value:当协程 co_yield 一个值时调用
    // 将值存储起来,并挂起协程
    std::suspend_always yield_value(T value) noexcept {
        value_ = std::move(value);
        return {};
    }

    // return_void:当协程没有返回特定值而结束时调用 (co_return; 或函数体结束)
    void return_void() const noexcept { /* 对于生成器,通常不需要做额外处理 */ }

    // unhandled_exception:当协程内部发生未捕获异常时调用
    // 捕获异常,以便在调用者恢复协程时重新抛出
    void unhandled_exception() noexcept {
        exception_ = std::current_exception();
    }

    // 获取当前 yield 的值
    T const& value() const noexcept { return value_; }

    // 检查是否有存储的异常,并重新抛出
    void rethrow_if_exception() {
        if (exception_) {
            std::rethrow_exception(exception_);
        }
    }
};

注意:generator<T>promise_type 通常被命名为 promise_type 并嵌套在 generator<T> 内部。为了避免循环依赖,我这里暂时将 promise_type 定义为 generator_promise<T>,并在 generator<T> 中使用 using promise_type = generator_promise<T>;

2. generator<T>::iterator

为了让 generator<T> 兼容范围for循环,我们需要实现一个迭代器类。这个迭代器需要能够:

  • 向前移动(operator++)。
  • 解引用以获取当前值(operator*)。
  • 进行比较(operator==, operator!=)以判断是否到达序列末尾。
// 迭代器类,用于遍历 generator
template <typename T>
struct generator_iterator {
    using coroutine_handle = std::coroutine_handle<generator_promise<T>>;

    coroutine_handle coro_handle_ = nullptr; // 关联的协程句柄

    // 默认构造函数,创建 end 迭代器
    generator_iterator() = default;

    // 构造函数,从协程句柄创建迭代器
    explicit generator_iterator(coroutine_handle handle) noexcept
        : coro_handle_(handle) {}

    // 比较运算符:判断两个迭代器是否相等
    bool operator==(generator_iterator const& other) const noexcept {
        return coro_handle_ == other.coro_handle_;
    }

    // 比较运算符:判断两个迭代器是否不相等
    bool operator!=(generator_iterator const& other) const noexcept {
        return !(*this == other);
    }

    // 解引用运算符:获取当前值
    T const& operator*() const {
        // 确保协程已挂起并产生了值
        // 这里没有直接检查 coro_handle_ 是否为 nullptr,因为 end 迭代器不应该被解引用
        // 如果 end 迭代器被解引用,它将是未定义行为
        return coro_handle_.promise().value();
    }

    // 前置递增运算符:恢复协程以获取下一个值
    generator_iterator& operator++() {
        coro_handle_.resume(); // 恢复协程
        // 如果协程已经结束,将句柄置空,使其成为 end 迭代器
        if (coro_handle_.done()) {
            coro_handle_ = nullptr;
        }
        // 检查是否有异常,如果有则抛出
        if (coro_handle_) { // 只有当协程仍然有效时才检查异常
             coro_handle_.promise().rethrow_if_exception();
        }
        return *this;
    }

    // 后置递增运算符:通常不建议对生成器使用,效率较低
    generator_iterator operator++(int) {
        generator_iterator temp = *this;
        ++(*this);
        return temp;
    }
};

3. generator<T> 主类

现在,我们将 promise_typeiterator 组合到 generator<T> 类中。

// generator 主类
template <typename T>
struct generator {
    // 声明 promise_type 为 generator_promise<T>
    using promise_type = generator_promise<T>;
    using iterator = generator_iterator<T>;
    using coroutine_handle = std::coroutine_handle<promise_type>;

private:
    coroutine_handle coro_handle_; // 协程句柄

public:
    // 默认构造函数
    generator() noexcept : coro_handle_(nullptr) {}

    // 从协程句柄构造 generator
    explicit generator(coroutine_handle handle) noexcept
        : coro_handle_(handle) {}

    // 移动构造函数
    generator(generator&& other) noexcept
        : coro_handle_(std::exchange(other.coro_handle_, nullptr)) {}

    // 移动赋值运算符
    generator& operator=(generator&& other) noexcept {
        if (this != &other) {
            if (coro_handle_) {
                coro_handle_.destroy(); // 销毁当前协程
            }
            coro_handle_ = std::exchange(other.coro_handle_, nullptr);
        }
        return *this;
    }

    // 禁用拷贝构造和拷贝赋值,因为协程句柄不能被拷贝
    generator(const generator&) = delete;
    generator& operator=(const generator&) = delete;

    // 析构函数:确保协程帧被销毁
    ~generator() {
        if (coro_handle_) {
            coro_handle_.destroy();
        }
    }

    // begin() 方法:返回指向序列第一个元素的迭代器
    // 第一次调用时,恢复协程以产生第一个值
    iterator begin() {
        if (!coro_handle_) {
            // 如果协程句柄为空,返回一个 end 迭代器
            return iterator{};
        }
        // 恢复协程,这将执行到第一个 co_yield 或 co_return
        coro_handle_.resume();
        // 检查是否有异常,如果有则抛出
        coro_handle_.promise().rethrow_if_exception();

        if (coro_handle_.done()) {
            // 如果协程在第一次恢复后立即结束,返回一个 end 迭代器
            coro_handle_ = nullptr; // 将句柄置空,确保析构时不会重复销毁
            return iterator{};
        }
        return iterator{coro_handle_};
    }

    // end() 方法:返回一个表示序列末尾的迭代器
    // 这是一个空迭代器,用于与 begin() 返回的迭代器进行比较
    iterator end() noexcept {
        return iterator{};
    }

    // 检查生成器是否有效 (是否有底层协程)
    explicit operator bool() const noexcept {
        return static_cast<bool>(coro_handle_);
    }
};

// 实现 generator_promise<T>::get_return_object()
// 需要在 generator<T> 定义之后,因为它返回 generator<T>
template <typename T>
generator<T> generator_promise<T>::get_return_object() {
    // std::coroutine_handle<promise_type>::from_promise(*this)
    // 从 promise 对象获取其关联的 coroutine_handle
    return generator<T>{std::coroutine_handle<generator_promise<T>>::from_promise(*this)};
}

至此,我们有了一个通用的 generator<T> 类型,它可以用于创建各种按需生成值的序列。

实现无限长度的斐波那契数列生成器

有了 generator<T> 框架,实现斐波那契数列生成器就非常简单了。我们只需要编写一个协程函数,该函数使用 co_yield 来产生斐波那契数列的每个数字。

为了处理“无限”长度,我们需要考虑数值溢出问题。long longstd::int64_t)或 unsigned long longstd::uint64_t)在达到其最大值后会溢出。斐波那契数列增长非常快,std::uint64_t 大约在第 93 个数字时就会溢出。

$F_{93} approx 1.22 times 10^{19}$
std::uint64_t::max() 是 $1.84 times 10^{19}$。
$F{94} = F{93} + F_{92} approx 1.22 times 10^{19} + 7.54 times 10^{18} approx 1.97 times 10^{19}$,这已经超过了 std::uint64_t 的最大值。

对于真正的“无限长度”且数值不限大小的斐波那契数列,我们需要一个任意精度(arbitrary-precision)的整数类型,例如 boost::multiprecision::cpp_int。为了保持核心概念的清晰,我们首先使用 std::uint64_t,并在后续讨论中提及任意精度整数。

#include <iostream>
#include <limits> // For std::numeric_limits

// 定义斐波那契生成器函数
// 它返回一个 generator<std::uint64_t> 对象
generator<std::uint64_t> fibonacci_sequence() {
    std::uint64_t a = 0;
    std::uint64_t b = 1;

    // 产生第一个值 0
    co_yield a;

    // 产生第二个值 1
    co_yield b;

    // 循环生成后续值
    while (true) {
        // 检查是否会溢出
        // 如果 b > ULLONG_MAX - a,则 a + b 将溢出
        if (std::numeric_limits<std::uint64_t>::max() - a < b) {
            // 抛出异常来指示溢出,或者 co_return 结束序列
            // 抛出异常是更明确的方式,因为生成器通常不应该“默默地”停止
            throw std::overflow_error("Fibonacci sequence overflowed std::uint64_t");
        }

        std::uint64_t next_fib = a + b;
        co_yield next_fib;

        a = b;
        b = next_fib;
    }
}

这个 fibonacci_sequence 协程函数将不断生成斐波那契数,直到 std::uint64_t 溢出为止。当溢出发生时,它会抛出一个 std::overflow_error 异常,这个异常会被 generator_promise::unhandled_exception() 捕获并存储,最终在调用者尝试获取下一个值时重新抛出。

使用斐波那契生成器

现在我们已经有了完整的 generator<T>fibonacci_sequence,我们可以像使用任何其他序列一样使用它。

// main 函数中演示如何使用斐波那契生成器
int main() {
    std::cout << "--- Generating first 10 Fibonacci numbers ---" << std::endl;
    int count = 0;
    for (std::uint64_t num : fibonacci_sequence()) {
        std::cout << num << " ";
        count++;
        if (count >= 10) {
            break; // 获取前10个数字后停止
        }
    }
    std::cout << "n" << std::endl;

    std::cout << "--- Generating Fibonacci numbers until overflow (up to 100 values) ---" << std::endl;
    count = 0;
    try {
        auto fib_gen = fibonacci_sequence(); // 创建一个生成器实例
        for (std::uint64_t num : fib_gen) {
            std::cout << "F[" << count << "] = " << num << std::endl;
            count++;
            if (count > 100) { // 为了避免打印太多,限制一下数量
                std::cout << "... stopping after 100 values for display purposes." << std::endl;
                break;
            }
        }
    } catch (const std::overflow_error& e) {
        std::cerr << "Caught exception: " << e.what() << " at F[" << count << "]" << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Caught unexpected exception: " << e.what() << std::endl;
    }
    std::cout << "n" << std::endl;

    std::cout << "--- Explicitly iterating and checking for validity ---" << std::endl;
    auto my_fib_gen = fibonacci_sequence();
    auto it = my_fib_gen.begin();
    auto end_it = my_fib_gen.end();

    try {
        for (int i = 0; i < 95; ++i) { // 尝试获取超过 93 个值,触发溢出
            if (it == end_it) {
                std::cout << "Generator exhausted prematurely." << std::endl;
                break;
            }
            std::cout << "F[" << i << "] = " << *it << std::endl;
            ++it;
        }
    } catch (const std::overflow_error& e) {
        std::cerr << "Explicit iteration caught exception: " << e.what() << std::endl;
    }

    return 0;
}

示例输出(部分):

--- Generating first 10 Fibonacci numbers ---
0 1 1 2 3 5 8 13 21 34 

--- Generating Fibonacci numbers until overflow (up to 100 values) ---
F[0] = 0
F[1] = 1
F[2] = 1
...
F[90] = 2880067194370816120
F[91] = 4660046610375530309
F[92] = 7540113804746346429
Caught exception: Fibonacci sequence overflowed std::uint64_t at F[93]

--- Explicitly iterating and checking for validity ---
F[0] = 0
F[1] = 1
...
F[92] = 7540113804746346429
Explicit iteration caught exception: Fibonacci sequence overflowed std::uint64_t

可以看到,当斐波那契数超过 std::uint64_t 的最大值时,我们的生成器正确地抛出了异常,并且调用者能够捕获并处理它。这证明了协程在按需生成数据和优雅处理终止条件方面的强大能力。

深入考量与最佳实践

1. 内存管理与协程帧

C++20 协程是“栈无关”(stackless)的。这意味着协程的局部变量和状态不是存储在调用栈上,而是存储在堆上分配的“协程帧”(coroutine frame)中。std::coroutine_handle 实际上是一个指向这个协程帧的指针。

  • 优点: 协程可以在任何时候挂起和恢复,不受调用栈深度的限制。协程帧的生命周期独立于调用它的函数。
  • 缺点/注意事项: 协程帧的内存分配和释放是有开销的。在我们的 generator 实现中,coro_handle_.destroy() 负责释放协程帧。确保 generator 对象的析构函数被调用,或者在不再需要时显式调用 destroy() 是至关重要的,否则会导致内存泄漏。我们的 generator 类通过移动语义和析构函数妥善处理了这一点。

2. 性能考量

与传统的迭代器或基于回调的生成器相比,C++20 协程的性能开销主要来自:

  • 协程帧的分配: 每次协程函数被调用时,都会在堆上分配一个协程帧。这比在栈上分配局部变量要慢。
  • 挂起/恢复操作: 每次 co_yield 都会涉及状态保存和恢复,以及控制流的切换。
  • promise_type 的构造/销毁: 协程生命周期中的 promise_type 操作。

对于生成少量元素的序列,传统循环或简单函数可能更快。但对于以下场景,协程的优势显著:

  • 懒加载(Lazy Evaluation): 仅在需要时计算下一个值,避免一次性计算和存储整个序列,节省内存。这对于无限序列或非常大的序列至关重要。
  • 复杂状态管理: 协程自然地保存了函数内部的局部状态,无需手动在类成员变量中维护复杂的迭代状态。
  • 异步操作: co_await 在异步编程中表现出色,可以将复杂的异步逻辑扁平化,避免回调地狱。

对于我们的斐波那契生成器,由于它可能生成大量甚至“无限”的数字,协程的懒加载特性使其成为一个非常合适的选择。每次迭代的开销是可接受的,因为我们避免了巨大的内存占用。

3. 任意精度整数

正如之前讨论的,std::uint64_t 在斐波那契数列的第 93 个数字左右就会溢出。如果我们需要一个真正“无限”且数值可以任意大的斐波那契数列,我们需要引入任意精度整数库。

一个流行的选择是 Boost 库中的 boost::multiprecision
使用 boost::multiprecision::cpp_int 替换 std::uint64_tfibonacci_sequence 函数将变得如下:

#include <boost/multiprecision/cpp_int.hpp> // 需要安装 Boost 库

// 假设 generator<T> 已定义并能与 boost::multiprecision::cpp_int 配合
generator<boost::multiprecision::cpp_int> fibonacci_sequence_arbitrary_precision() {
    boost::multiprecision::cpp_int a = 0;
    boost::multiprecision::cpp_int b = 1;

    co_yield a;
    co_yield b;

    while (true) {
        // Boost.Multiprecision 会自动处理溢出,提供任意大的整数
        boost::multiprecision::cpp_int next_fib = a + b;
        co_yield next_fib;

        a = b;
        b = next_fib;
    }
}

使用任意精度整数,斐波那契数列将理论上无限增长,直到系统内存耗尽(因为数字本身会越来越大,存储它们需要更多内存)。但在实际应用中,这通常已经足够满足“无限”的需求。

4. 异常处理

我们的 generator 实现了基本的异常传递机制:当协程内部抛出异常时,promise_type::unhandled_exception() 会捕获它。当调用者尝试通过 operator++ 恢复协程时,rethrow_if_exception() 会重新抛出该异常。这使得协程的错误处理与普通函数保持一致,调用者可以在协程外部捕获并处理异常。

5. 协程的取消(Cancellation)

对于无限生成器,有时我们可能需要在生成器仍在运行时主动停止它。C++20 协程本身没有内置的取消机制,但我们可以通过以下方式实现:

  • 外部标志: 在协程中检查一个外部共享标志。

    std::atomic<bool> stop_flag = false;
    
    generator<std::uint64_t> cancellable_fibonacci_sequence(std::atomic<bool>& stop_signal) {
        std::uint64_t a = 0;
        std::uint64_t b = 1;
    
        if (stop_signal.load()) co_return; // 初始检查
        co_yield a;
    
        if (stop_signal.load()) co_return;
        co_yield b;
    
        while (true) {
            if (stop_signal.load()) {
                co_return; // 如果收到停止信号,则结束协程
            }
            // ... 斐波那契计算和溢出检查 ...
            std::uint64_t next_fib = a + b;
            co_yield next_fib;
            a = b;
            b = next_fib;
        }
    }

    调用者可以在另一个线程中设置 stop_flag,然后协程在下次恢复时检查该标志并退出。

  • 破坏 generator 对象: 最简单直接的方式是销毁 generator 对象本身。当 generator 对象析构时,它会调用 coro_handle_.destroy(),从而清理协程帧并停止协程。对于生成器模式,这通常是默认且推荐的取消方式。

6. 与其他生成器模式的比较

  • 基于回调的生成器: 需要将生成逻辑分散到多个回调函数中,状态管理复杂。
  • 手写迭代器: 需要手动实现 begin(), end(), operator++, operator* 等,并且需要一个类来存储状态。协程生成器将迭代逻辑和状态管理自然地融合在一个函数中,大大简化了代码。
  • Boost.Coroutine(Stackful): Boost 库提供了栈式协程,允许协程拥有自己的栈。这在某些场景下更灵活,但通常性能开销更大,并且不属于 C++ 标准。C++20 协程是栈无关的,通常更轻量。

C++20 协程提供了一种更简洁、更直观的方式来实现生成器模式,尤其适用于需要暂停和恢复执行流的场景,如无限序列、异步操作和管道化数据处理。

C++20 协程的潜在陷阱

尽管 C++20 协程功能强大,但在使用时也需要注意一些潜在的陷阱:

  1. 协程句柄的生命周期管理: std::coroutine_handle 只是一个指针,它不拥有协程帧的内存。如果 coroutine_handle 在协程帧被销毁后仍被使用,将导致悬空指针。我们的 generator 类通过移动语义和析构函数中的 coro_handle_.destroy() 确保了正确的生命周期管理。
  2. promise_type 的实现错误: promise_type 的各个方法(特别是 initial_suspend, final_suspend, yield_value)的返回值和逻辑必须正确,否则协程的行为将不符合预期,可能导致无限循环、提前终止或内存泄漏。
  3. 捕获变量的生命周期: 如果协程捕获了局部变量的引用(通过 [&]),并且这些局部变量在协程挂起后被销毁,那么当协程恢复时,它将访问悬空引用,导致未定义行为。因此,在协程中最好按值捕获变量([=])或确保捕获的引用所指向的对象在协程的整个生命周期内都有效。对于斐波那契生成器,ab 是协程的局部变量,存储在协程帧中,因此它们在挂起和恢复之间是安全的。
  4. 忘记 co_awaitco_yield 如果一个函数被设计为协程,但忘记使用 co_await, co_yieldco_return,它将不会被编译器识别为协程,而是普通函数,导致编译错误或意外行为。
  5. begin()/end() 的语义: 对于生成器,end() 迭代器通常是一个默认构造的空迭代器。当 begin() 恢复协程后,如果协程立即完成(例如,一个空的生成器),begin() 应该返回 end() 迭代器。我们的实现已经考虑了这一点。

展望 C++ 协程的未来

C++20 协程提供的是底层原语。这意味着我们需要像 generator<T> 这样的自定义类型来封装这些原语,以提供更高级、更易用的抽象。未来,标准库可能会提供更多的协程友好类型,例如 std::generator(目前在 C++23/26 提案中),这将进一步简化协程的使用。

协程在异步编程领域也具有巨大潜力,例如在网络服务器、事件循环和图形用户界面中。通过 co_await,我们可以编写看起来像同步代码的异步代码,极大地提高了可读性和可维护性。

协程构建懒惰序列的强大能力

通过本次讲座,我们不仅深入了解了 C++20 协程的核心机制,还成功地构建了一个功能强大的无限长度斐波那契数列生成器。协程的引入为 C++ 带来了处理异步操作和实现懒惰求值序列的全新范式,显著提升了代码的简洁性、可读性和效率。掌握协程,无疑将为 C++ 开发者开启更多高级编程的可能性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注