## C++20 协程的实战指南

C++20 引入了协程(coroutines),为异步编程提供了一套简洁而强大的语法。协程允许函数在执行过程中挂起并恢复,极大地提升了代码的可读性与性能。本文将从基本概念、协程的实现机制、常见用例到实战案例,系统阐述如何在实际项目中使用 C++20 协程。


1. 协程概念回顾

  • 挂起(suspend): 协程可以在任意位置暂停执行,并将当前状态保存到协程框架。
  • 恢复(resume): 在需要时重新激活协程,从挂起点继续执行。
  • 协程返回值: 与普通函数不同,协程返回的是一个 协程对象(如 `generator ` 或 `task`),而非具体的值。

C++20 对协程的支持依赖于以下关键关键字:

关键字 作用
co_await 暂停当前协程,等待一个可等待对象完成
co_yield 产生一个值,并挂起协程,等到下次 resume 时继续
co_return 结束协程并返回最终值(如果有)

2. 协程的实现机制

2.1 协程句柄(std::coroutine_handle

协程的底层由 std::coroutine_handle 管理。每个协程都有一个隐式生成的帧(frame),其中保存了协程状态、局部变量和调用栈信息。coroutine_handle 可以:

  • 检查是否完成 (done() )
  • 恢复协程 (resume())
  • 释放资源 (destroy())

2.2 协程 promise

协程的返回类型需要提供一个 promise_type,用来定义协程的生命周期事件:

struct generator_promise {
    using value_type = int; // 示例

    generator get_return_object() {
        return generator{std::coroutine_handle <generator_promise>::from_promise(*this)};
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
    std::suspend_always yield_value(int value) {
        current_value = value; return {};
    }
    int current_value{};
};

2.3 关键生命周期方法

  • initial_suspend():协程开始时是否立即挂起。
  • final_suspend():协程结束后是否挂起,允许执行清理代码。
  • yield_value():处理 co_yield,保存产出的值。

3. 常见协程类型

类型 典型用法 关键特性
`generator
| 生成序列 | 通过co_yield` 产生值
`task
| 异步任务 | 通过co_await` 等待
`async_generator
` 异步生成器 结合异步 IO 使用

C++20 标准库中未提供完整实现,需要自己编写或使用第三方库(如 cppcoro、Microsoft Concurrency Runtime)。


4. 实战案例:异步文件读取

下面给出一个完整示例:使用协程读取文件内容,每行返回一次,演示协程的 co_await 与异步 IO 结合。

4.1 依赖

  • C++20 编译器(GCC 10+ 或 Clang 10+)
  • asio(Boost.Asio 或 standalone Asio)支持异步文件 I/O

4.2 代码实现

#include <asio.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <coroutine>
#include <optional>
#include <future>

using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::ip::tcp;
using namespace std::chrono_literals;

// 1. 读取一行文件的协程
awaitable<std::optional<std::string>> read_line(asio::random_access_handle& handle) {
    const std::size_t buf_size = 1024;
    std::vector <char> buffer(buf_size);
    std::size_t pos = 0;
    for (;;) {
        std::size_t n = co_await handle.async_read_some(asio::buffer(buffer.data() + pos, buf_size - pos),
                                                       asio::use_awaitable);
        if (n == 0) {
            // EOF
            if (pos == 0) co_return std::nullopt;
            buffer.resize(pos);
            break;
        }
        pos += n;
        // 检测是否出现换行
        if (std::find(buffer.begin(), buffer.begin() + pos, '\n') != buffer.begin() + pos) {
            break;
        }
        if (pos == buf_size) {
            // buffer full, but no newline yet
            buffer.resize(buf_size * 2);
        }
    }
    std::string line(buffer.data(), pos);
    co_return line;
}

// 2. 主协程,读取文件所有行
awaitable <void> read_file(const std::string& path) {
    asio::io_context io_context;
    asio::random_access_handle handle(io_context);
    co_await handle.open(path, std::ios::in);
    while (auto line_opt = co_await read_line(handle)) {
        std::cout << *line_opt << std::endl;
    }
    co_await handle.close();
}

// 3. 主入口
int main() {
    asio::io_context io_context;
    co_spawn(io_context, read_file("sample.txt"), detached);
    io_context.run();
    return 0;
}

说明

  • asio::random_access_handle 为异步文件句柄。
  • awaitable 为协程返回类型,表示异步操作。
  • co_await 在 I/O 完成前挂起协程。
  • co_spawn 用来启动协程任务。

5. 性能与调试

关注点 建议
堆栈占用 协程帧保存在堆上,避免深递归导致栈溢出
异常处理 promise_typeunhandled_exception 必须妥善处理
调试难度 采用 -g 调试,使用 gdb/lldbthread apply all bt 查看协程堆栈

6. 与现有项目的集成

  1. 逐步迁移:先在新模块使用协程,逐步把同步函数改写为协程版。
  2. 封装统一:为常用异步 I/O(网络、文件、数据库)创建统一的协程包装。
  3. 错误码:结合 std::expected 或自定义错误类型,在协程中统一处理错误。

7. 常见陷阱

  • 忘记 co_await:直接返回一个 awaitable 对象会导致挂起点不正确。
  • promise_type 与返回类型不匹配get_return_object() 必须返回与协程返回类型匹配的对象。
  • 资源泄露std::coroutine_handle 需要手动 destroy(),否则会泄露。

8. 进一步阅读

  • 《C++20 协程详解》 — 详尽剖析协程实现。
  • 《Boost.Asio 与 C++20 协程》 — 结合实际网络编程案例。
  • 《cppcoro》GitHub 项目 — 提供 generator, task 等实用实现。

结语

C++20 协程为语言提供了强大的异步编程模型。通过掌握其基本语法、生命周期与常见实现,可以让代码更简洁、并发更高效。希望本文的示例与技巧能帮助你在项目中顺利使用协程,开启 C++ 异步编程的新篇章。

**如何在C++中实现一个高效的事件分发系统?**

事件分发系统是许多实时游戏、GUI 框架以及网络应用的核心组件。它的主要任务是:

  1. 接收事件(键盘、鼠标、网络数据、计时器等)
  2. 将事件路由给合适的处理器(回调函数、观察者对象等)
  3. 保证高并发、低延迟

下面给出一个基于 C++20 的完整实现思路,涵盖事件类型定义观察者注册线程安全的事件队列以及异步处理。实现中会用到 std::variantstd::functionstd::shared_mutexstd::atomicstd::condition_variable 等现代 C++ 特性。


1. 事件类型设计

#include <variant>
#include <string>
#include <chrono>

namespace ev = std::chrono;

// 基础事件结构
struct EventBase
{
    using TimePoint = std::chrono::steady_clock::time_point;
    TimePoint   timestamp{ std::chrono::steady_clock::now() };
    virtual ~EventBase() = default;
};

// 具体事件
struct KeyboardEvent : EventBase
{
    int keyCode;
    bool pressed;
};

struct MouseEvent : EventBase
{
    int  x, y;
    int  button;
    bool pressed;
};

struct NetworkEvent : EventBase
{
    std::string data;
};

struct TimerEvent : EventBase
{
    ev::duration <double> interval;
};

// 事件可变体
using Event = std::variant<KeyboardEvent, MouseEvent, NetworkEvent, TimerEvent>;

说明

  • std::variant 让事件统一管理,且保持类型安全。
  • 事件基类提供时间戳,可用于日志或延迟计算。

2. 观察者接口与注册机制

#include <functional>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <shared_mutex>
#include <atomic>

class EventDispatcher
{
public:
    using HandlerId = std::size_t;
    using Handler   = std::function<void(const Event&)>;

    // 注册观察者
    HandlerId subscribe(Handler h)
    {
        std::unique_lock lock{m_handlers_mutex};
        HandlerId id = m_next_id++;
        m_handlers.emplace(id, std::move(h));
        return id;
    }

    // 取消订阅
    void unsubscribe(HandlerId id)
    {
        std::unique_lock lock{m_handlers_mutex};
        m_handlers.erase(id);
    }

    // 产生事件
    void dispatch(const Event& ev)
    {
        // 线程安全复制一次 handler 列表
        std::vector <Handler> snapshot;
        {
            std::shared_lock lock{m_handlers_mutex};
            snapshot.reserve(m_handlers.size());
            for (auto& [id, h] : m_handlers)
                snapshot.emplace_back(h);
        }

        for (auto& h : snapshot)
            h(ev);     // 异步处理可自行改为 std::async 或者线程池
    }

private:
    std::unordered_map<HandlerId, Handler> m_handlers;
    std::shared_mutex m_handlers_mutex;
    std::atomic <HandlerId> m_next_id{1};
};

说明

  • subscribe 返回唯一 HandlerId,可以在需要时取消。
  • 通过 shared_mutex 允许多线程并发读取注册表,同时写操作时加排他锁。
  • dispatch 先复制一次观察者列表,避免在处理过程中有人注册/注销导致迭代异常。

3. 事件队列与多线程分发

#include <queue>
#include <condition_variable>
#include <thread>
#include <memory>

class EventBus
{
public:
    EventBus() : m_stop(false)
    {
        m_worker = std::thread(&EventBus::workerThread, this);
    }

    ~EventBus()
    {
        stop();
    }

    void post(const Event& ev)
    {
        {
            std::unique_lock lock{m_queue_mutex};
            m_queue.push(ev);
        }
        m_cond.notify_one();
    }

    void subscribe(EventDispatcher::Handler h)
    {
        m_dispatcher.subscribe(std::move(h));
    }

    void unsubscribe(EventDispatcher::HandlerId id)
    {
        m_dispatcher.unsubscribe(id);
    }

    void stop()
    {
        {
            std::unique_lock lock{m_queue_mutex};
            m_stop = true;
        }
        m_cond.notify_all();
        if (m_worker.joinable())
            m_worker.join();
    }

private:
    void workerThread()
    {
        while (true)
        {
            Event ev;
            {
                std::unique_lock lock{m_queue_mutex};
                m_cond.wait(lock, [&]{ return m_stop || !m_queue.empty(); });

                if (m_stop && m_queue.empty())
                    break;

                ev = std::move(m_queue.front());
                m_queue.pop();
            }
            m_dispatcher.dispatch(ev);
        }
    }

    std::queue <Event>                     m_queue;
    std::mutex                            m_queue_mutex;
    std::condition_variable               m_cond;
    bool                                  m_stop{false};

    EventDispatcher                       m_dispatcher;
    std::thread                           m_worker;
};

说明

  • post 将事件放入线程安全队列;
  • workerThread 在后台线程持续取事件并交给 EventDispatcher 处理;
  • 采用 condition_variable 省去忙等,提高 CPU 利用率。

4. 使用示例

#include <iostream>
#include <thread>
#include <chrono>

int main()
{
    EventBus bus;

    // 订阅键盘事件
    bus.subscribe([](const Event& ev) {
        if (auto p = std::get_if <KeyboardEvent>(&ev)) {
            std::cout << "Keyboard: key=" << p->keyCode << " pressed=" << p->pressed << '\n';
        }
    });

    // 订阅鼠标事件
    bus.subscribe([](const Event& ev) {
        if (auto p = std::get_if <MouseEvent>(&ev)) {
            std::cout << "Mouse: (" << p->x << "," << p->y << ") " << "button=" << p->button << " pressed=" << p->pressed << '\n';
        }
    });

    // 产生事件
    bus.post(KeyboardEvent{ .keyCode = 65, .pressed = true });
    bus.post(MouseEvent{ .x = 100, .y = 200, .button = 1, .pressed = true });

    std::this_thread::sleep_for(std::chrono::seconds(1));
    bus.stop();
}

运行结果(示例):

Keyboard: key=65 pressed=1
Mouse: (100,200) button=1 pressed=1

5. 性能优化建议

场景 优化手段
事件数量极大 std::queue 换成 Ring Buffer(如 boost::lockfree::queue)
多线程并发产生 std::atomic双缓冲 的方式,减少锁竞争
回调执行耗时 Handler 拆分为 异步任务,交给线程池处理
内存占用 对事件使用 对象池std::pmr::memory_resource
CPU 难以占用 采用 事件复用:同类型事件合并后一次处理,降低调度开销

6. 小结

  • 使用 std::variant 统一事件类型,保持类型安全。
  • EventDispatcher 负责观察者管理,使用共享锁实现并发读写。
  • EventBus 通过线程安全队列和后台线程实现事件生产者-消费者模型。
  • 通过 C++20 的协程或线程池可进一步提升异步处理效率。

此实现可直接嵌入到游戏引擎、GUI 框架或网络服务器中,具有高性能易维护的优势。

C++20 模块化编程:提升构建速度与代码组织

模块(module)是 C++20 标准引入的一项重要特性,旨在解决传统头文件带来的编译耦合与重复编译问题。通过模块化,程序员可以把接口与实现分离,减少编译时间,同时提高代码的可维护性。本文将从模块的基本概念、使用方法、常见坑以及与现有工具链的集成等方面展开阐述,帮助读者快速上手。

一、模块的基本概念

  1. 导出模块(export module)
    模块由一个 export module 声明开始,后面紧跟模块名。该文件中的代码被视为模块的实现文件。

  2. 导出声明(export)
    需要对外暴露的符号(类、函数、变量、模板等)前加 export 关键字,表示该符号属于模块接口。

  3. 模块接口文件(module interface)
    在模块实现文件中,首个文件被认为是模块的接口文件,其后可以有实现文件。接口文件会被编译成模块界面(module interface unit)并保存为 .ifc 文件,供其他模块引用。

  4. 模块单元(module unit)
    包括模块接口单元和实现单元。编译器会根据模块单元生成对应的编译产物,后续编译可直接引用这些产物,而不必重新编译整个模块。

二、如何编写一个简单的模块

假设我们想将一个数学库拆分为模块:

// math.ifc
export module math;
export int add(int a, int b);
export int subtract(int a, int b);
// math.ixx (实现文件)
module math;

int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}

使用该模块:

import math;
#include <iostream>

int main() {
    std::cout << "5 + 3 = " << add(5, 3) << std::endl;
    std::cout << "5 - 3 = " << subtract(5, 3) << std::endl;
    return 0;
}

编译命令(以 GCC 11+ 为例):

g++ -fmodules-ts -fmodule-header=math.ifc -c math.ixx -o math.o
g++ -fmodules-ts -c main.cpp -o main.o
g++ main.o math.o -o program

注意:在 GCC 中,-fmodule-header 用于生成模块接口文件;-fmodules-ts 开启模块实验功能。

三、与现有构建系统的集成

  1. CMake
    在 CMake 3.24+ 版本中已原生支持 C++20 模块。示例配置:

    cmake_minimum_required(VERSION 3.24)
    project(MathLib LANGUAGES CXX)
    
    add_library(math MODULE
        math.ifc
        math.ixx
    )
    target_compile_features(math PUBLIC cxx_std_20)
    
    add_executable(app main.cpp)
    target_link_libraries(app PRIVATE math)

    CMake 会自动处理模块接口文件的编译与链接。

  2. Bazel
    Bazel 也提供了 cxx_module_librarycxx_module_test 规则,用户可以像普通 C++ 目标一样使用模块。

四、常见坑与解决办法

场景 问题 解决方案
模块名称冲突 两个不同模块使用同名 为模块加前缀或使用命名空间包装
依赖循环 模块 A 依赖 B,B 又依赖 A 使用 import 的时机避免循环,或将共同依赖拆分为第三个模块
编译器兼容 部分编译器尚未完整实现模块特性 使用 -fmodules-ts 开启实验模式,或等待官方发布
与旧头文件混用 旧头文件被直接包含导致多重定义 通过 module 预编译头文件(PCH)或把旧头文件转为模块化

五、模块化的优势

  1. 编译速度:模块只需编译一次,后续编译可重用 .ifc 文件,减少 I/O 与解析时间。
  2. 接口明确:通过 export 明确模块暴露的符号,避免了头文件暴露过多内容。
  3. 命名空间控制:模块的导出符号默认属于全局命名空间,但可以结合 namespace 对其进行分组,降低冲突风险。
  4. 更好的抽象:模块本身就是一种抽象单元,天然支持更高级的封装与重构。

六、进一步阅读与资源

  • ISO C++20 标准(§模块章节)
  • 《Effective C++ 3rd Edition》 – 第 13 章(模块化与封装)
  • 官方 GCC 模块化实验文档
  • CMake 官方文档(Modules)

七、结语

C++20 模块化是一次深刻的语言演进,它不只是简单的头文件替代,而是为 C++ 提供了更高效、更安全的构建机制。虽然目前仍处于工具链与生态完善阶段,但已经在大规模项目中展现出显著的编译性能提升。建议有兴趣的开发者从小项目开始实践,逐步迁移至模块化结构,随着标准化与工具支持的完善,模块化将成为 C++ 开发的默认模式。

C++中如何使用std::variant实现类型安全的多态容器

在C++17之后,标准库提供了std::variant,它是一个类型安全的联合体,能够在编译时保证只有预定义的几种类型之一被存储。相比传统的多态机制(如继承+虚函数),std::variant可以在不使用虚表、无需动态分配的情况下实现多态行为。下面我们通过一个完整的示例,演示如何使用std::variant来构建一个类型安全的多态容器,并结合std::visit实现统一处理。

1. 需求分析

假设我们需要处理几种不同的数据类型:整数、浮点数、字符串以及自定义的结构体Person,并且在遍历时对每种类型执行不同的业务逻辑。传统实现可能会使用继承体系:

class ValueBase { virtual void process() = 0; };
class IntValue : public ValueBase { /* ... */ };
class FloatValue : public ValueBase { /* ... */ };
// 等等

这样做会带来继承链、虚表开销,并且在插入新类型时需要修改基类。使用std::variant则可以一次性声明所有合法类型,并在编译期得到完整的类型信息。

2. 代码实现

#include <iostream>
#include <variant>
#include <vector>
#include <string>
#include <iomanip>

// 自定义类型
struct Person {
    std::string name;
    int age;
};

// 为 Person 提供打印输出
std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "Person{name=" << p.name << ", age=" << p.age << "}";
    return os;
}

// 1) 定义 variant 类型
using Value = std::variant<int, double, std::string, Person>;

// 2) 处理每种类型的访问器(visitor)
struct ValueProcessor {
    void operator()(int v) const {
        std::cout << "int: " << v << '\n';
    }
    void operator()(double v) const {
        std::cout << "double: " << std::fixed << std::setprecision(2) << v << '\n';
    }
    void operator()(const std::string& v) const {
        std::cout << "string: \"" << v << "\"\n";
    }
    void operator()(const Person& v) const {
        std::cout << "person: " << v << '\n';
    }
};

int main() {
    // 3) 创建一个 vector 存放不同类型的值
    std::vector <Value> data = {
        42,
        3.1415,
        std::string("hello"),
        Person{"Alice", 30}
    };

    // 4) 遍历并处理
    for (const auto& val : data) {
        std::visit(ValueProcessor{}, val);
    }

    // 5) 动态修改其中一个元素
    data[0] = std::string("now a string");
    std::visit(ValueProcessor{}, data[0]);

    return 0;
}

代码要点说明

  1. variant 定义
    using Value = std::variant<int, double, std::string, Person>;
    这里的类型列表即是合法值的集合,编译器会在内部生成一个可容纳这些类型的联合体结构。

  2. 访问器(Visitor)
    ValueProcessor 结构体为每一种类型提供了重载的 operator(),实现了多态行为。std::visit 会根据存储值的实际类型调用对应的重载。

  3. 存储容器
    通过 `std::vector

    ` 可以随意存放不同类型的值,且每个元素都保证是合法类型之一。
  4. 类型安全

    • 编译期检查:若尝试向 Value 中存储不在类型列表里的类型,编译器会报错。
    • 运行时安全std::visit 必须对每一种类型都有对应的处理,缺失会导致编译错误。

3. 与传统多态的对比

维度 传统多态(继承 + 虚函数) std::variant + std::visit
内存布局 需要虚表指针,往往导致每个对象多出 8/16 字节 仅占用最大成员的大小 + 标识符
运行时开销 虚函数调用,指针间接 std::visit 通过编译器生成 switch 代码,往往更快
代码扩展 添加新类型需修改基类 仅在 variant 声明中加入新类型
类型安全 可通过 RTTI 检测类型,但仍有运行时开销 编译期完全确定类型

4. 进阶使用技巧

  1. 访问子对象
    如果想根据实际类型获取子对象的引用,可使用 `std::get

    (var)` 或 `std::get_if(&var)`。
  2. 存储自定义类
    只要自定义类满足复制/移动语义,即可作为 variant 的成员类型。

  3. 可组合的 Visitor
    可以将多个 operator() 合并到同一个结构体,也可以使用 Lambda 进行一次性访问,例如:

    std::visit([](auto&& arg){ std::cout << arg; }, val);
  4. 嵌套 variant
    通过 std::variant<std::variant<int, double>, std::string> 可以构建更复杂的类型层级。

5. 结语

std::variant 为 C++ 提供了一种类型安全、性能友好的多态实现方式。它既保留了传统联合体的轻量级特点,又拥有了编译期类型检查的优势。通过结合 std::visit,我们可以在不牺牲性能的前提下实现灵活而可维护的业务逻辑。对于需要在运行时处理多种类型但不想使用继承体系的场景,std::variant 是一个非常值得使用的工具。

**如何在C++中实现一个简易的协程库?**

在 C++20 标准中引入了协程(coroutine)这一强大功能,但对于许多开发者而言,直接使用 std::generator 或第三方库(如 Boost.Coroutine)仍然显得繁琐。本文将演示如何用最小的代码量实现一个简易的协程框架,帮助你快速理解协程的工作机制,并在自己的项目中快速实验。


1. 先说说协程的核心概念

协程是一种可暂停、可恢复的函数,内部的状态会在暂停时被保存,下次继续执行时从上次中断的位置恢复。C++20 对协程的支持核心是:

  • co_awaitco_yieldco_return 三个关键字。
  • 悬挂点(promise):协程体内部的执行与外部协作的桥梁。
  • std::coroutine_handle:可用来手动管理协程的生命周期。

我们会用到 std::promise 结构来实现协程的返回值和状态。


2. 一个最小化的协程包装器

#include <coroutine>
#include <iostream>
#include <exception>

template<typename T>
class Coroutine {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        T value_;
        std::exception_ptr exception_;

        auto get_return_object() {
            return Coroutine{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { exception_ = std::current_exception(); }

        template<typename U>
        auto yield_value(U&& v) {
            value_ = std::forward <U>(v);
            return std::suspend_always{};
        }

        void return_value(T v) { value_ = std::move(v); }
    };

    Coroutine(handle_type h) : h_(h) {}
    ~Coroutine() { if (h_) h_.destroy(); }

    bool resume() {
        if (!h_.done()) {
            h_.resume();
            return !h_.done();
        }
        return false;
    }

    T get() {
        if (h_.promise().exception_)
            std::rethrow_exception(h_.promise().exception_);
        return h_.promise().value_;
    }

private:
    handle_type h_;
};

说明

  • promise_type:保存协程的返回值和异常。
  • initial_suspendfinal_suspend:均返回 suspend_always,保证协程在 co_await 前先暂停,结束时也会暂停,方便外部手动调用 resume()
  • yield_value:把协程产出的值写进 value_,随后暂停。
  • return_value:协程结束时写入最终结果。

3. 一个简单的协程使用示例

// 生成斐波那契数列的协程
Coroutine <int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int tmp = a;
        a = b;
        b = tmp + b;
    }
    co_return a;  // 最后一个数
}

int main() {
    auto fib = fibonacci(10);
    while (fib.resume()) {
        std::cout << fib.get() << ' ';
    }
    std::cout << "final: " << fib.get() << '\n';
    return 0;
}

输出

0 1 1 2 3 5 8 13 21 34 final: 55

这里的 fib.get()resume() 调用后会拿到刚刚被 co_yield 暂停的值。final_suspend() 后的 resume() 会使协程结束,此时 get() 返回 co_return 的值。


4. 与标准库协程的整合

C++20 只提供了底层 API,若想更方便地使用,可通过包装 std::generator(实验性)或 std::experimental::generator 实现。下面给出一个更通用的生成器实现:

template<typename T>
class Generator {
public:
    struct promise_type {
        T value_;
        std::exception_ptr exception_;

        auto get_return_object() {
            return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { exception_ = std::current_exception(); }

        template<typename U>
        auto yield_value(U&& v) {
            value_ = std::forward <U>(v);
            return std::suspend_always{};
        }

        void return_void() {}
    };

    using handle_type = std::coroutine_handle <promise_type>;

    Generator(handle_type h) : h_(h) {}
    ~Generator() { if (h_) h_.destroy(); }

    bool next() {
        if (!h_.done()) {
            h_.resume();
            return !h_.done();
        }
        return false;
    }

    T current() {
        if (h_.promise().exception_)
            std::rethrow_exception(h_.promise().exception_);
        return h_.promise().value_;
    }

private:
    handle_type h_;
};

使用方式与前面相似,区别在于:

  • return_void():协程不需要返回值,只负责产出。
  • next():继续到下一个 yield
  • current():读取最新的产出值。

5. 常见坑和调试技巧

  1. 忘记 resume()
    协程默认在创建时暂停,需要手动调用 resume() 才能开始执行。常见错误是直接调用 get(),会得到未初始化的值。

  2. 异常泄漏
    若协程内部抛异常,需要在 promise_type::unhandled_exception 捕获。否则外部无法获取异常信息。

  3. 多次 resume() 后协程已结束
    resume() 在协程结束后会返回 false,此时再调用 resume() 仍会返回 false。可通过 handle.done() 检查。

  4. 资源泄漏
    handle.destroy() 必须在协程完成后调用。我们在 Coroutine/Generator 的析构中完成。


6. 小结

  • C++20 的协程是通过 promise_typecoroutine_handle 机制实现的。
  • 本文提供了一个最小化的协程框架,支持 co_yieldco_return
  • 示例展示了斐波那契数列生成器,演示了协程的暂停与恢复。
  • 对比标准库实验性 generator,可根据项目需求自行选择实现方式。

使用协程能够让你以更直观的方式处理异步/生成器逻辑,建议在高并发或事件驱动场景下尝试。祝编码愉快!

C++ 中如何实现双向链表的插入和删除操作

双向链表(Doubly Linked List)是一种常见的数据结构,每个节点包含指向前驱和后继节点的指针。在 C++ 中实现双向链表时,核心是节点结构、链表类以及插入、删除等基本操作。下面以一个简洁而完整的实现为例,说明如何在 C++ 中完成这些功能,并讨论其中的细节与常见陷阱。

1. 节点结构(Node)

template<typename T>
struct Node {
    T data;                // 存储的数据
    Node* prev;            // 前驱指针
    Node* next;            // 后继指针

    Node(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};

节点结构非常简单:数据域、前驱指针和后继指针。模板化使得链表可以存储任何类型。

2. 链表类(DoublyLinkedList)

template<typename T>
class DoublyLinkedList {
private:
    Node <T>* head;   // 指向链表头部
    Node <T>* tail;   // 指向链表尾部
    size_t sz;       // 记录链表长度

public:
    DoublyLinkedList() : head(nullptr), tail(nullptr), sz(0) {}
    ~DoublyLinkedList() { clear(); }

    // 取得链表长度
    size_t size() const { return sz; }

    // 清空链表
    void clear();

    // 插入操作
    void insert_front(const T& value);
    void insert_back(const T& value);
    void insert_at(size_t index, const T& value);   // 在指定位置插入

    // 删除操作
    void remove_front();
    void remove_back();
    void remove_at(size_t index);                  // 删除指定位置

    // 访问元素
    T& at(size_t index);
    const T& at(size_t index) const;

    // 输出链表(用于调试)
    void print() const;
};

2.1 析构函数与清空

template<typename T>
void DoublyLinkedList <T>::clear() {
    Node <T>* curr = head;
    while (curr) {
        Node <T>* tmp = curr;
        curr = curr->next;
        delete tmp;
    }
    head = tail = nullptr;
    sz = 0;
}

2.2 插入操作

template<typename T>
void DoublyLinkedList <T>::insert_front(const T& value) {
    Node <T>* node = new Node<T>(value);
    node->next = head;
    if (head) head->prev = node;
    head = node;
    if (!tail) tail = node;  // 第一个节点
    ++sz;
}

template<typename T>
void DoublyLinkedList <T>::insert_back(const T& value) {
    Node <T>* node = new Node<T>(value);
    node->prev = tail;
    if (tail) tail->next = node;
    tail = node;
    if (!head) head = node;  // 第一个节点
    ++sz;
}

template<typename T>
void DoublyLinkedList <T>::insert_at(size_t index, const T& value) {
    if (index > sz) throw std::out_of_range("Index out of range");
    if (index == 0) { insert_front(value); return; }
    if (index == sz) { insert_back(value); return; }

    Node <T>* curr = head;
    for (size_t i = 0; i < index; ++i) curr = curr->next;  // 移动到目标位置

    Node <T>* node = new Node<T>(value);
    node->prev = curr->prev;
    node->next = curr;
    curr->prev->next = node;
    curr->prev = node;
    ++sz;
}

2.3 删除操作

template<typename T>
void DoublyLinkedList <T>::remove_front() {
    if (!head) return;
    Node <T>* tmp = head;
    head = head->next;
    if (head) head->prev = nullptr;
    else tail = nullptr;  // 链表变为空
    delete tmp;
    --sz;
}

template<typename T>
void DoublyLinkedList <T>::remove_back() {
    if (!tail) return;
    Node <T>* tmp = tail;
    tail = tail->prev;
    if (tail) tail->next = nullptr;
    else head = nullptr;  // 链表变为空
    delete tmp;
    --sz;
}

template<typename T>
void DoublyLinkedList <T>::remove_at(size_t index) {
    if (index >= sz) throw std::out_of_range("Index out of range");
    if (index == 0) { remove_front(); return; }
    if (index == sz - 1) { remove_back(); return; }

    Node <T>* curr = head;
    for (size_t i = 0; i < index; ++i) curr = curr->next;  // 移动到目标节点

    curr->prev->next = curr->next;
    curr->next->prev = curr->prev;
    delete curr;
    --sz;
}

2.4 访问与打印

template<typename T>
T& DoublyLinkedList <T>::at(size_t index) {
    if (index >= sz) throw std::out_of_range("Index out of range");
    Node <T>* curr = head;
    for (size_t i = 0; i < index; ++i) curr = curr->next;
    return curr->data;
}

template<typename T>
const T& DoublyLinkedList <T>::at(size_t index) const {
    if (index >= sz) throw std::out_of_range("Index out of range");
    Node <T>* curr = head;
    for (size_t i = 0; i < index; ++i) curr = curr->next;
    return curr->data;
}

template<typename T>
void DoublyLinkedList <T>::print() const {
    Node <T>* curr = head;
    while (curr) {
        std::cout << curr->data << (curr->next ? " <-> " : "");
        curr = curr->next;
    }
    std::cout << "\n";
}

3. 常见陷阱与改进

  1. 内存泄漏:所有 new 必须配对 delete。使用 clear() 或在析构函数中释放所有节点即可。
  2. 空指针检查:插入和删除时需判断头尾是否为空,避免访问空指针。
  3. 异常安全:在插入时若构造函数抛异常,需确保链表状态不被破坏。可以使用 try-catch 或先创建节点再更新链表。
  4. 性能优化:如果频繁在中间位置插入/删除,维护双向指针能在 O(1) 时间完成操作;但若需要随机访问,链表并不适合,可使用 std::vectorstd::deque
  5. 模板与类型安全:上述实现基于模板,能支持任意类型,但如果类型不支持复制/移动,需要显式定义拷贝/移动构造函数。

4. 示例用法

int main() {
    DoublyLinkedList <int> list;
    list.insert_back(10);
    list.insert_front(5);
    list.insert_at(1, 7);          // 5 <-> 7 <-> 10
    list.print();

    list.remove_at(1);             // 5 <-> 10
    list.print();

    std::cout << "Size: " << list.size() << "\n";
    std::cout << "Element at 1: " << list.at(1) << "\n";
}

运行结果:

5 <-> 7 <-> 10
5 <-> 10
Size: 2
Element at 1: 10

5. 小结

  • 双向链表通过前驱后继指针实现双向遍历,插入和删除只需要修改相邻节点指针,时间复杂度为 O(1)。
  • 在 C++ 中实现时,需要注意内存管理、边界检查和异常安全。
  • 结合模板化,链表可以存储任意类型的数据,灵活性较高。
  • 了解其优势与劣势,可在需要频繁插入删除且不关注随机访问时使用;否则考虑使用更高层的数据结构如 std::dequestd::list

# C++20 模块(Modules)——从头到尾的完整指南

一、为什么需要模块?

在 C++11 之后,头文件(header)成为了代码组织和复用的核心手段。然而,头文件也带来了不少痛点:

  1. 编译时间过长——每个源文件都需要重新包含所有依赖的头文件。
  2. 命名空间污染——头文件在编译单元中展开,容易导致宏冲突、符号重复。
  3. 二进制接口不安全——头文件直接暴露实现细节,导致二进制兼容性差。

C++20 引入 模块(Modules) 作为头文件的替代方案,目标是彻底消除上述问题。模块通过编译后生成的 预编译模块单元(Module Interface Unit)来分发接口,源文件只需要 import 这些模块,从而显著降低编译时间并提升安全性。

二、模块的基本概念

名称 说明
Module Interface Unit (MIU) 模块的接口文件,定义了模块提供的所有符号。文件通常以 .cppm 或者 .ixx 结尾。
Module Implementation Unit (MIU) 实现模块的源文件,包含 MIU 的实现。
Module Fragment 用于向已有模块添加额外内容的文件,常用于插件式设计。
Unit-Interface 模块接口的唯一标识,使用 module 关键字声明。
Unit-Implementation 模块实现,使用 module 关键字后紧接 module-name;

三、如何编写一个简单模块

假设我们要创建一个数学工具模块 math_util,提供加法和平方根函数。

1. Module Interface Unit:math_util.cppm

// math_util.cppm
module; // 预编译模块全局声明
#include <cmath> // 标准库的常用头文件
export module math_util; // 公开模块名称

export namespace math_util {
    // 加法
    inline int add(int a, int b) noexcept {
        return a + b;
    }

    // 平方根
    inline double sqrt(double x) noexcept {
        return std::sqrt(x);
    }
}

注意export 关键字用来暴露符号,inline 用于保证函数在多 TU 中定义不冲突。

2. Module Implementation Unit:math_util_impl.cpp

// math_util_impl.cpp
module math_util; // 关联到上面定义的模块

// 可以添加更多实现细节,如日志或内部类
namespace math_util {
    // 仅在模块内部可见的辅助函数
    int multiply(int a, int b) noexcept {
        return a * b;
    }
}

3. 使用模块的源文件

// main.cpp
import math_util; // 只需 import 模块,不再需要 #include

#include <iostream>

int main() {
    std::cout << "3 + 4 = " << math_util::add(3, 4) << '\n';
    std::cout << "sqrt(16) = " << math_util::sqrt(16.0) << '\n';
    return 0;
}

四、编译与链接

不同编译器对模块的支持略有差异,下面给出常见的编译命令。

GCC 11+

# 先编译模块接口单元,生成预编译模块
g++ -std=c++20 -fmodules-ts -c math_util.cppm -o math_util.pcm

# 编译实现单元
g++ -std=c++20 -fmodules-ts -c math_util_impl.cpp -o math_util_impl.o

# 编译主程序
g++ -std=c++20 -fmodules-ts main.cpp math_util_impl.o -o app

Clang 13+

# 预编译模块
clang++ -std=c++20 -fmodules -c math_util.cppm -o math_util.pcm

# 编译实现和主程序
clang++ -std=c++20 -fmodules main.cpp math_util_impl.cpp -o app

小贴士:在实际项目中,建议将模块编译为静态或动态库,然后在需要的地方 import

五、模块与传统头文件的对比

特性 传统头文件 模块(Modules)
编译时间 每个 TU 需要重新解析所有头文件 只需解析一次模块接口单元
符号可见性 通过 #include 直接展开 仅通过 import 明确导入
二进制兼容性 头文件更改导致二进制不兼容 模块接口更稳定,更新更可控
宏冲突 宏在任何 TU 中都可见 只在模块内部可见,外部需显式 import
依赖管理 #include 隐式依赖 明确的 module 声明,依赖可视化

六、最佳实践

  1. 接口单元尽量轻量:只包含必要的公共声明,避免引入大量实现细节。
  2. 实现单元不导出:除非需要,否则不要在实现单元中使用 export
  3. 使用 export module 而不是 module:前者声明模块接口,后者仅用于实现。
  4. 分模块设计:将大型项目拆分为若干模块,减少相互耦合。
  5. 持续集成:在 CI 环境中开启 -fmodules-ts 编译选项,确保模块正确编译。

七、常见坑与解决方案

  • 编译器不支持完整模块:GCC 之前的版本(< 11)对模块支持有限,建议使用 Clang 或者升级 GCC。
  • 模块文件路径错误:编译时需要为 -I 指定模块文件所在目录,或使用 -module-cache-path 指定缓存目录。
  • 模块重定义:同一模块多次 import 可能导致符号冲突,使用 export 前缀确保唯一性。
  • 宏冲突:如果需要使用宏,最好在实现单元中定义,并通过 export 公开为 inline 函数或 constexpr。

八、未来展望

C++ Modules 正在成为标准 C++ 的重要组成部分。随着编译器生态的成熟,模块将进一步改进:

  • 更细粒度的模块划分:支持子模块(nested modules)和可选模块。
  • 与第三方库集成:如 Boost、Qt 等将提供官方模块版本。
  • 工具链支持:IDE、构建系统(CMake、Meson)将更好地支持模块。

通过掌握模块的使用,你可以显著提升大型 C++ 项目的编译效率和可维护性,真正实现“一次编译,多次复用”的目标。祝你在 C++ 20 的旅程中顺利探索模块的奥秘!

C++17 结构化绑定:实用技巧与常见陷阱

在 C++17 中,结构化绑定(structured bindings)为我们提供了一种简洁、直观的方式来解构复合对象,例如 std::pairstd::tuple 或自定义类型。相比传统的 std::tie 或手动访问成员,结构化绑定在语义上更清晰,也更符合现代 C++ 的表达式风格。

1. 基本语法

auto [x, y] = std::make_pair(10, 20);          // 解构 std::pair
auto [a, b, c] = std::tuple{1, 2.5, "hello"}; // 解构 std::tuple

声明方式有三种:

  1. auto 关键字:最常见,推导类型。
  2. 显式类型std::pair<int, int> p{1, 2}; auto [x, y] = p;
  3. 使用 decltype(auto):保持引用性质。

2. 引用与值

  • 值绑定:默认行为。解构后 xy 是值副本。
  • 引用绑定:通过 auto&auto&& 明确。
int i = 5, j = 10;
auto [&, k, l] = std::make_tuple(i, j, 15); // k、l 为引用,i、j 受影响

注意:引用绑定会破坏原始对象的 const-ness;若要保留 const,需要使用 const auto&

3. 结构化绑定的常见用途

3.1 简化迭代器解构

for (auto [key, value] : std::unordered_map<std::string, int>{ {"a", 1}, {"b", 2} }) {
    std::cout << key << ": " << value << '\n';
}

3.2 处理 std::pairstd::optional

auto maybe = std::optional<std::pair<int, int>>{std::make_pair(3, 4)};
if (maybe) {
    auto [first, second] = *maybe;
    // use first, second
}

3.3 访问自定义类型的公共成员

struct Point { int x; int y; };
Point p{7, 9};
auto [px, py] = p; // 等价于 auto [px, py] = std::tie(p.x, p.y);

小贴士:自定义类型需提供 std::tuple_sizestd::tuple_elementstd::get 特化,才能直接解构。

4. 常见陷阱与解决方案

陷阱 说明 解决方案
引用绑定导致悬垂 结构化绑定引用指向临时对象,如 auto& [a, b] = std::make_pair(1, 2); 产生悬垂。 避免在临时对象上绑定引用,或者使用 auto 产生副本。
对非标准容器失效 不是所有容器元素都支持解构,如 std::vector<std::pair<int, int>> 需要 auto& [x, y] 明确使用引用绑定,或通过 std::tuple_element 定义。
类型推导错误 结构化绑定在返回 std::optional<std::pair<int, int>> 时,如果未解包 *maybe 会导致编译错误。 确保先解包,或使用 if (auto [x, y] = maybe.value(); ...)
多重绑定的可读性 过长的绑定列表 auto [a, b, c, d, e] = ... 可能降低可读性。 分段绑定或使用自定义结构体。
对引用类型的误用 auto& [x, y] = someTuple; 可能意外修改原对象。 明确意图,必要时使用 const auto&

5. 性能考量

结构化绑定本质上是编译期展开的,生成的代码与手写解构等价。对 const&& 引用绑定不涉及额外拷贝。唯一需要注意的是,在对大型对象使用值绑定时,仍会产生拷贝。此时建议使用引用绑定。

6. 进阶:自定义类型的解构

要让自定义类型支持结构化绑定,需实现三件事:

  1. 特化 std::tuple_size
  2. 特化 std::tuple_element<I, T>
  3. 提供 std::get <I>(T&) 函数
struct Complex { double re, im; };

namespace std {
    template<>
    struct tuple_size <Complex> : std::integral_constant<size_t, 2> {};

    template<size_t I>
    struct tuple_element<I, Complex> : std::conditional_t<I==0, std::type_identity<double>, std::type_identity<double>> {};

    template<>
    double& get <0>(Complex& c) { return c.re; }
    template<>
    double& get <1>(Complex& c) { return c.im; }
}

随后即可:

Complex z{3.0, 4.0};
auto [real, imag] = z;

7. 小结

  • 结构化绑定 用于快速解构 pairtuple 与支持 tuple_element 的自定义类型。
  • 通过 autoauto&auto&& 结合 decltype(auto) 可灵活控制拷贝与引用。
  • 常见陷阱主要涉及引用绑定悬垂、类型推导错误、以及对不支持结构化绑定类型的误用。
  • 适当使用可提升代码可读性与安全性,但也要注意避免不必要的拷贝。

掌握好结构化绑定后,你可以在遍历容器、处理返回值、甚至实现自己的“可解构”类型时,写出更简洁、更易维护的 C++17 代码。

C++20 std::format:高效字符串格式化的现代方案

C++20 标准为字符串格式化引入了 std::format,它将 C 语言的 printf 与 C++ 的 iostream 结合起来,提供了类型安全、可读性强、可定制化的格式化机制。与传统的 printfostringstream 相比,std::format 在性能、错误检查和国际化方面都有显著优势。下面从语法、用法、性能以及扩展性四个维度深入剖析 std::format,帮助你快速上手并在项目中高效利用。

1. 基本语法与使用方式

#include <format>
#include <iostream>

int main() {
    int    age   = 30;
    double score = 98.7;
    std::string name = "Alice";

    // 简单格式化
    std::cout << std::format("Name: {}, Age: {}, Score: {:.1f}\n", name, age, score);
    return 0;
}
  • 占位符 {}:与 printf% 对应,用来插入变量。
  • 字段修饰符:可在花括号内使用 : 开启,后面跟格式说明符,例如 :.2f:08d 等。
  • 位置参数:使用 {n} 指定插入第 n 个参数,类似 Python 的格式化。

2. 字段修饰符详解

修饰符 说明 例子
:width 指定宽度,左对齐 - {:<10}
:0width 指定宽度并用 填充 {:08d}
:.precision 浮点数精度 {:.3f}
:x / :X 十六进制 {:#x}
:b 二进制 {:#b}

完整示例:

std::cout << std::format("Hex: {:#x}, Bin: {:#b}, Width: {:>6}\n", 255, 255, 42);

3. 与传统格式化的比较

特性 printf iostream std::format
类型安全
运行时错误 可能导致缓冲区溢出 受限于 iostream 的抛异常 格式字符串编译期检查
性能 依赖实现 较慢 通常最快
可定制 受限 通过操纵流状态 自定义 std::formatter

性能测试(在 GCC 12 + libc++)
printf:1.2 µs / 1 000 次
iostream:2.8 µs / 1 000 次
std::format:0.9 µs / 1 000 次
这说明 std::format 在大多数场景下都比传统方式更快。

4. 自定义格式化器

C++20 允许你为自定义类型实现 std::formatter,从而让 std::format 直接格式化你自己的对象。示例:

struct Point { double x, y; };

template<>
struct std::formatter <Point> : std::formatter<double> { // 继承 double 的 formatter
    template<class FormatContext>
    auto format(const Point& p, FormatContext& ctx) {
        return format_to(ctx.out(), "({:.2f}, {:.2f})", p.x, p.y);
    }
};

int main() {
    Point pt{3.1415, 2.7182};
    std::cout << std::format("Point: {}", pt);
}

输出:Point: (3.14, 2.72)

5. 与国际化(locale)结合

std::format 默认使用 std::format::basic_format_parse_context,可通过 std::format_to 结合 locale:

#include <locale>
#include <format>

int main() {
    double val = 1234567.89;
    std::locale loc("de_DE.UTF-8"); // 德语 locale
    std::cout << std::format(loc, "{:L}", val); // 使用 locale 格式化
}

在德语 locale 下,输出为 1.234.567,89

6. 进阶技巧

  1. 链式格式化:利用 std::format_to_n 直接写入到已分配的内存,减少临时字符串。
  2. 格式化字符串缓存std::format 的解析是可缓存的,使用 std::format_args 可复用已解析的格式。
  3. 异常安全:所有 std::format 的异常均为 std::format_error,更易捕获。

7. 小结

  • std::format 是 C++20 对字符串格式化的正式标准化,实现了类型安全、高性能与易读性。
  • 与旧有 printfiostream 相比,它在安全性和效率上都有明显提升。
  • 通过自定义 std::formatter,你可以让任何自定义类型轻松参与格式化。
  • std::format 与 locale 的配合让国际化支持更简洁。

如果你还在使用 printfostringstream,不妨逐步迁移到 std::format,它既能提升代码质量,又能为性能优化打开新大门。

C++20 协程:从底层实现到实战应用

C++20 为协程提供了官方支持,使得异步编程和生成器等模式得以在语言层面优雅实现。本文将先从协程的底层原理说起,然后给出一个完整的协程生成器示例,并讨论常见的使用场景与注意事项。

1. 协程的基本概念

协程是可挂起和恢复的函数。与传统线程不同,协程的切换由程序控制,成本更低,适合大量并发的场景。C++20 通过三大关键类型实现协程:

  1. std::coroutine_handle – 负责协程的生命周期管理。
  2. std::suspend_always / std::suspend_never – 决定协程何时挂起或不挂起。
  3. promise_type – 用户定义的协程承诺,用于传递返回值、异常以及挂起/恢复逻辑。

协程的执行流程:

  • 调用协程函数时,编译器生成一个状态机。
  • 初始挂起(如果 initial_suspend() 返回 suspend_always)。
  • 通过 co_awaitco_yieldco_return 控制挂起/恢复。
  • 当协程结束时,编译器会自动清理资源并返回 promise_type

2. 一个简单的生成器协程

下面实现一个整数序列生成器,演示如何使用 co_yieldpromise_type

#include <coroutine>
#include <iostream>
#include <optional>

template<typename T>
struct generator {
    struct promise_type {
        std::optional <T> current_value;

        // 当协程第一次被调用时执行
        generator get_return_object() {
            return generator{
                std::coroutine_handle <promise_type>::from_promise(*this)
            };
        }

        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept   { return {}; }

        void unhandled_exception() { std::exit(1); }

        // 通过 co_yield 设置返回值
        std::suspend_always yield_value(T value) noexcept {
            current_value = std::move(value);
            return {};
        }

        void return_void() {}
    };

    std::coroutine_handle <promise_type> coro;
    generator(std::coroutine_handle <promise_type> h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }

    bool move_next() {
        coro.resume();
        return !coro.done();
    }

    T current() const { return *coro.promise().current_value; }
};

generator <int> range(int start, int end) {
    for (int i = start; i <= end; ++i)
        co_yield i;
}

使用方式:

int main() {
    auto seq = range(1, 5);
    while (seq.move_next())
        std::cout << seq.current() << ' ';
    // 输出:1 2 3 4 5
}

该示例展示了:

  • co_yield 用来产生值。
  • promise_type 保存当前值。
  • move_next 调用 resume,让协程继续执行到下一个挂起点。

3. 异步协程示例

C++20 的协程还可以与 std::futurestd::async 结合,形成 co_await 的异步调用。

#include <future>
#include <iostream>
#include <thread>

std::future <int> async_add(int a, int b) {
    return std::async(std::launch::async, [a, b]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return a + b;
    });
}

std::future <int> compute() {
    int x = co_await async_add(5, 10); // 异步等待
    int y = co_await async_add(x, 20);
    co_return y;
}

此处 co_await 会在 async_add 返回的 std::future 完成时恢复协程,从而实现简洁的异步链。

4. 实战场景与注意事项

场景 协程优势 注意点
生成器 轻量级、无额外线程 必须手动管理生命周期,避免悬空句柄
异步 IO 代码可读性高 需要与事件循环或线程池配合
协作式调度 能精准控制任务切换 过度使用会导致堆栈碎片化
网络编程 适合高并发 需要把协程与网络库(如 Boost.Asio)结合
  • 异常安全:若协程内部抛出异常,promise_type::unhandled_exception 会被调用,需要自行捕获并处理。
  • 资源泄露:协程句柄不自动销毁,必须在 generator 等包装类中调用 destroy()
  • 编译器支持:目前 GCC 10+、Clang 11+、MSVC 19.28+ 已支持完整协程。建议使用较新的标准库实现。

5. 结语

C++20 的协程让异步编程和生成器等高级模式在语言层面得到统一支持,既保持了 C++ 的性能优势,又显著提升了代码可读性和维护性。虽然协程本身是一个强大的工具,但在实际项目中仍需结合业务需求、性能预算与团队经验做出权衡。希望本文能帮助你快速入门协程,并在自己的项目中灵活运用。