C++ 23 标准中的协程:从语法到实践

协程(coroutine)是 C++20 开始被正式纳入标准的强大特性,它提供了比传统线程和回调更简洁、可读性更高的异步编程方式。C++23 在此基础上做了进一步的完善和优化,让协程的使用更加方便。本文将从语法、关键概念、实现细节以及实际案例四个方面,系统梳理 C++23 协程的核心内容,帮助读者快速上手。


1. 语法与基本结构

1.1 关键字和基本形态

C++23 协程与 C++20 的语法保持一致,核心关键词依旧是 co_await, co_yield, co_return,以及 co_resume(用于手动恢复)。协程函数的返回类型需要是 协程返回类型(co-routine return type),常见的包括 `std::generator

`、`std::task` 等。 “`cpp std::generator count_up_to(int n) { for (int i = 1; i ` 与 `std::suspend_always` / `std::suspend_never` 的简易实现。 “`cpp struct my_promise { int current = 0; std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } int get_return_object() { return current; } void unhandled_exception() { std::terminate(); } void return_void() {} }; “` ### 1.3 协程的创建与销毁 协程的创建由编译器生成 `__coro` 结构体完成,调用协程函数返回的是对应的 Promise 对象包装器。销毁过程会调用 `final_suspend` 并释放资源。 — ## 2. 协程的核心概念 ### 2.1 协程状态 协程的状态主要包括: – **Suspended**:已暂停,等待恢复。 – **Running**:正在执行。 – **Completed**:已完成,无法再恢复。 协程在 `co_await`、`co_yield` 或 `co_return` 处暂停,随后由外部或内部恢复。 ### 2.2 Awaitable 与 Awaiter `co_await` 后面的表达式必须满足 *awaitable* 接口。C++23 提供了默认的 `std::suspend_always` / `std::suspend_never`,也支持自定义 Awaiter。 “`cpp struct async_task { bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle h) const noexcept { // 异步操作开始,完成后恢复协程 } int await_resume() const noexcept { return 42; } }; int main() { std::coroutine_handle h = my_coroutine(); // 简化示例 int result = co_await async_task(); // 这里会暂停 } “` ### 2.3 协程的异常处理 异常会沿着协程的调用链向上传递,直到到达 `return` 或 `co_return`。如果协程未捕获异常,Promise 的 `unhandled_exception` 会被调用。 — ## 3. C++23 对协程的改进 ### 3.1 更强的协程返回类型 C++23 引入了 `std::generator ` 与 `std::task`,分别对应可生成多值和单值的协程。`std::generator` 在迭代器模型上兼容标准容器,使得协程可以像普通 `range` 一样使用。 “`cpp std::generator lines(const std::string& path) { std::ifstream file(path); std::string line; while (std::getline(file, line)) { co_yield line; } } “` ### 3.2 `std::suspend_always` 与 `std::suspend_never` 的默认化 C++23 允许在协程返回类型中使用 `std::suspend_always` 或 `std::suspend_never` 作为默认暂停策略,减少模板代码量。 ### 3.3 更细粒度的资源管理 通过 `std::coroutine_handle` 的 `destroy()` 方法以及 Promise 的 `final_suspend`,可以更精细地控制协程资源的释放,避免泄漏。 — ## 4. 实践案例:异步文件读取 下面演示一个使用 C++23 协程实现的异步文件读取框架。 “`cpp #include #include #include #include #include #include #include // 简化的 awaitable,用于模拟异步 I/O struct async_read_line { std::ifstream& file; std::string line; bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle h) const noexcept { std::thread([this, h]() { if (std::getline(file, line)) { h.resume(); // I/O 完成后恢复协程 } else { h.resume(); // EOF,直接恢复 } }).detach(); } std::optional await_resume() const noexcept { if (line.empty()) return std::nullopt; return line; } }; std::generator async_file_reader(const std::string& path) { std::ifstream file(path); if (!file) throw std::runtime_error(“Cannot open file”); while (true) { auto maybe_line = co_await async_read_line{file}; if (!maybe_line) break; // EOF co_yield *maybe_line; } } int main() { try { for (const auto& line : async_file_reader(“sample.txt”)) { std::cout

C++20 模块化编程:从头到尾的完整示例

在 C++20 中,模块(Modules)被引入为一种新的编译单元机制,旨在取代传统的头文件和源文件分离的方式,解决多重包含、编译时间长以及命名冲突等问题。下面我们从设计思路、实现细节到完整示例,逐步演示如何使用 C++20 模块化编程。

1. 设计思路

  • 模块化的核心:将声明和实现分别封装在一个模块(module)中,模块提供 导出(export)接口给外部使用。外部程序通过 import 关键字引用模块。
  • 模块的好处
    • 编译加速:编译器只需要一次性解析模块接口,后续包含相同接口时无需重复编译。
    • 封装性:只暴露需要对外的符号,隐藏内部实现细节。
    • 依赖管理:模块之间的依赖关系显式声明,避免了隐藏的头文件依赖。

2. 模块的基本语法

  • 模块定义

    export module math;        // 声明模块名称
    export module math::utils; // 子模块(可选)
  • 导出接口

    export struct Vector {
        double x, y, z;
    };
    
    export double dot(const Vector&, const Vector&);
  • 内部实现(不需要 export):

    double dot(const Vector& a, const Vector& b) {
        return a.x*b.x + a.y*b.y + a.z*b.z;
    }
  • 使用模块

    import math;   // 引入 math 模块
    // 或者
    import math::utils;

3. 具体实现:一个三维向量库

3.1 模块文件:vector.mod.cpp

// vector.mod.cpp
export module vector; // 主模块

export struct Vector3D {
    double x{}, y{}, z{};

    // 构造函数
    explicit constexpr Vector3D(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {}

    // 向量加法
    constexpr Vector3D operator+(const Vector3D& other) const {
        return Vector3D{x + other.x, y + other.y, z + other.z};
    }

    // 向量点乘
    constexpr double dot(const Vector3D& other) const {
        return x * other.x + y * other.y + z * other.z;
    }

    // 归一化
    constexpr Vector3D normalize() const {
        double len = std::sqrt(dot(*this));
        return Vector3D{x / len, y / len, z / len};
    }
};

// 实用函数,导出
export constexpr double magnitude(const Vector3D& v) {
    return std::sqrt(v.dot(v));
}

说明:

  • export module vector; 定义了一个名为 vector 的模块。
  • 所有 export 关键字后的声明(如 struct Vector3Dmagnitude)对外可见。
  • 函数体不需要 export,仅声明需要暴露的符号。

3.2 主程序文件:main.cpp

// main.cpp
import vector;   // 引入 vector 模块
import std.core; // 标准库模块

int main() {
    Vector3D a(1.0, 2.0, 3.0);
    Vector3D b(4.0, 5.0, 6.0);

    Vector3D c = a + b;
    double dot_product = a.dot(b);
    double mag_a = magnitude(a);
    Vector3D normalized_a = a.normalize();

    std::cout << "c: (" << c.x << ", " << c.y << ", " << c.z << ")\n";
    std::cout << "a · b = " << dot_product << '\n';
    std::cout << "|a| = " << mag_a << '\n';
    std::cout << "normalized a: (" << normalized_a.x << ", " << normalized_a.y << ", " << normalized_a.z << ")\n";
    return 0;
}

4. 编译与运行

使用支持 C++20 模块的编译器(如 GCC 11+、Clang 14+、MSVC 19.28+)。示例命令(GCC):

# 编译模块
g++ -std=c++20 -fmodules-ts -c vector.mod.cpp -o vector.o

# 编译主程序,链接模块对象
g++ -std=c++20 -fmodules-ts main.cpp vector.o -o main

运行:

./main

输出示例:

c: (5, 7, 9)
a · b = 32
|a| = 3.74166
normalized a: (0.267261, 0.534522, 0.801784)

注意:不同编译器的模块支持细节略有差异,必要时使用 -fmodules-ts 或相应选项开启实验性模块支持。

5. 进阶话题

  1. 模块间依赖

    export module math.geometry;
    import vector; // 依赖 vector 模块
    // ...
  2. 模块化与 CMake
    使用 CMake 的 target_sourcestarget_compile_options,配合 CMAKE_CXX_STANDARDCMAKE_CXX_EXTENSIONS OFF 可实现更细粒度的模块编译。

  3. 模块与 ABI
    C++20 模块在 ABI 层面有显著改进,但与传统头文件方式兼容性仍需注意,尤其是跨编译器使用时。

6. 小结

  • 模块是 C++20 里一个重要的语言特性,解决了头文件带来的重复编译、命名冲突等痛点。
  • 通过 export module 声明模块,export 导出接口,import 引入使用。
  • 示例演示了如何实现一个简单的三维向量库,并在主程序中使用,完整展示了模块化编译与链接过程。

随着编译器的成熟与工具链的完善,模块化编程将在大型项目中扮演越来越重要的角色,为 C++ 开发者提供更高效、更安全的编译体验。

**C++20 中的 std::span:轻量级视图与内存安全**

在 C++20 中引入了 std::span,它是一种轻量级的、非拥有的数组视图(array view)。相比传统的指针 + 长度,std::span 能够更安全、更直观地处理连续内存块,并能无缝与 STL 容器、C 风格数组、裸指针互操作。本文将从设计理念、典型用法、内存安全性、常见陷阱以及性能对比等方面,系统阐述 std::span 的优势与实践。


1. 设计理念与基本特性

关键特性 说明
非拥有 std::span 只存储指向外部数据的指针和长度,不负责管理内存。
固定长度 默认 `span
的长度是可变的(dynamic_extent),但可通过span` 指定固定长度。
构造兼容 可以从 std::array<T, N>std::vector<T>、C 风格数组、裸指针 + 长度等构造。
安全访问 提供 at()operator[]front()back() 等成员,支持越界检查(调试模式下)。
切片操作 subspan()first()last() 等方法,能够像字符串一样做子视图。
与算法兼容 STL 算法接受 std::span,例如 std::sort(span)

2. 典型用法

2.1 作为函数参数

void process(span <int> data) {
    for (auto& val : data) val *= 2;
}

// 调用
std::vector <int> vec{1, 2, 3, 4};
process(vec);                     // 直接传递 vector
process(std::array<int, 4>{5,6,7,8}); // 直接传递 array
int arr[] = {9,10,11,12};
process(arr);                     // C 风格数组
process(&arr[0], 4);              // 指针 + 长度

2.2 子视图(子数组)

span <int> all = vec; // 视图整个 vector
span <int> firstHalf = all.first(all.size()/2);   // 前一半
span <int> lastHalf  = all.last(all.size()/2);    // 后一半
span <int> middle = all.subspan(2, 3);            // 从索引 2 开始,长度 3

2.3 读取只读数据

const std::span<const T> 只允许读操作,适用于只读函数:

int sum(span<const int> data) {
    int total = 0;
    for (int v : data) total += v;
    return total;
}

3. 内存安全性与生命周期

由于 std::span 不拥有数据,使用时必须确保被引用的数据在 span 生命周期内有效。常见的安全策略:

  1. 与容器一起使用
    把 span 作为函数参数或返回值时,确保它指向的容器(如 vector、array)在使用期间保持生存。例如,函数 `process(span

    data)` 只能处理传入的临时容器,不能返回 span 指向局部 vector。
  2. 使用 std::arraystd::vectordata()
    vectordata() 指针在 vector 的容量不变时是稳定的,除非 push_back 超出容量触发重分配。

  3. 在栈上创建临时视图
    span 本身在栈上存储指针与长度,使用 auto s = make_span(arr); 只要 arr 存在即可。

  4. 避免返回局部数组的 span

    span <int> bad() {
        int local[5] = {1,2,3,4,5};
        return make_span(local);  // UB: local 失效
    }

4. 常见陷阱

陷阱 说明
超出范围访问 虽然 operator[] 不做检查,at() 会检查;在调试模式下 at() 可捕获越界。
空视图 `span
empty{}` 表示长度为 0,访问任何元素都会崩溃。
不匹配类型 传递 std::vector<const int>span<int> 会报错,需使用 span<const int>
错误的子视图参数 subspan 的起始索引 + 长度必须不超过原视图大小,否则 UB。

5. 性能对比

方法 说明 性能
T* ptr, std::size_t len 原生指针 + 长度 极简,开销为 2 个成员
`std::span
` 包装指针 + 长度 与指针+长度等价,少量类型安全
`std::vector
` 动态数组 有额外容量信息和析构成本
std::array<T, N> 固定数组 内存连续,但长度已编译时确定

std::span 的优势在于:

  • 类型安全:编译器能检查元素类型是否匹配。
  • 可读性:`span ` 明确表示“一个整数数组视图”。
  • 兼容性:与 STL 算法天然兼容,避免手动传递指针+长度。
  • 轻量化:仅占用 16 bytes(两个 8 字节成员),与裸指针差异极小。

6. 小结

std::span 为 C++20 带来了一种统一且安全的数组视图机制。它让函数签名更简洁,减少了错误传递长度的风险,并与 STL 算法无缝衔接。只要遵循“非拥有”和“生命周期一致”两条准则,即可在项目中安全、高效地使用 std::span。在实际编码中,推荐:

  1. 首选 span:如果只需要读取或修改连续内存块而不负责所有权,使用 std::span
  2. 避免返回局部视图:只返回指向已持久化容器的 span。
  3. 对性能要求极高的场景:仍可使用裸指针+长度,或在 std::span 上做微调。

希望本文能帮助你更好地理解并掌握 std::span 的使用。祝编码愉快!

如何在C++中实现一个线程安全的单例模式?

在多线程环境下,单例模式的实现往往需要考虑线程安全问题。下面从设计思想、常见实现方式以及C++17及以后语言特性等几个角度,对线程安全单例模式进行详细讲解,并给出完整可编译的示例代码。

1. 设计目标与挑战

  • 全局唯一性:保证在程序生命周期内,某类只能有一个实例。
  • 懒初始化:仅在首次使用时才创建实例,节省资源。
  • 线程安全:在多线程并发访问时,避免出现多实例、竞争条件或死锁。
  • 性能友好:初始化完成后,后续获取实例的开销尽可能小。

2. 经典实现方式

2.1 基于双重检查锁(Double-Check Locking)

class Singleton {
public:
    static Singleton* Instance() {
        if (!instance_) {                  // ①第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_) {              // ②第二次检查
                instance_ = new Singleton();
            }
        }
        return instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance_;
    static std::mutex mutex_;
};
  • 优点:只在第一次创建时进行锁操作,后续获取实例开销极低。
  • 缺点:需要注意内存可见性,C++11开始 std::atomicstd::mutex 能保证可见性;若使用裸指针可能导致重排序问题。

2.2 局部静态变量(Meyers’ Singleton)

class Singleton {
public:
    static Singleton& Instance() {
        static Singleton instance;   // C++11 保证线程安全初始化
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
  • 优点:实现极简,编译器保证初始化线程安全,且不需要手动维护锁。
  • 缺点:在C++03时代不保证线程安全;如果实例需要在 atexit 前手动销毁,可能产生析构顺序问题。

2.3 递归锁 + std::call_once

class Singleton {
public:
    static Singleton& Instance() {
        std::call_once(init_flag_, []() {
            instance_ = new Singleton();
        });
        return *instance_;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance_;
    static std::once_flag init_flag_;
};
  • 优点:使用 std::call_once 能一次性保证初始化,避免多次检查。
  • 缺点:需要手动释放 instance_,可以结合 std::unique_ptr 自动管理。

3. C++17 之后的改进

C++17 引入了 inline static 变量,使得在类内部声明静态成员并初始化变得更加简洁:

class Singleton {
public:
    static Singleton& Instance() {
        static Singleton instance;   // 依然是 Meyers' Singleton
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    inline static std::atomic <bool> initialized{false};
};

使用 inline 让编译器在每个翻译单元都生成同一实例,解决了多模块时可能的 ODR(One Definition Rule)冲突。

4. 常见陷阱

  1. 析构顺序:若单例在程序结束时析构,且其它全局对象依赖它,析构顺序可能导致访问悬空对象。可通过 std::atexit 注册释放或使用 std::shared_ptr 管理生命周期。
  2. 多线程竞态:即使采用 static 局部变量,C++11 之前的编译器仍不保证线程安全。务必使用现代编译器或显式锁。
  3. 死锁风险:如果单例在构造期间访问了其他单例,可能出现死锁。尽量避免在构造函数中调用其它单例。

5. 完整示例:日志单例

#include <iostream>
#include <fstream>
#include <mutex>
#include <string>

class Logger {
public:
    static Logger& Instance() {
        static Logger instance;   // Meyers' Singleton
        return instance;
    }

    void Log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        ofs_ << msg << '\n';
    }

private:
    Logger() : ofs_("log.txt", std::ios::app) {
        if (!ofs_) {
            throw std::runtime_error("无法打开日志文件");
        }
    }
    ~Logger() = default;
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    std::ofstream ofs_;
    std::mutex mutex_;
};

int main() {
    Logger::Instance().Log("程序启动");
    // 在多线程环境中可直接调用 Logger::Instance()
    return 0;
}

此实现利用了 C++11 的线程安全局部静态变量,并通过互斥锁保证多线程写日志时的互斥。整个生命周期内只有一个 Logger 实例,满足单例需求。

6. 小结

  • Meyers’ Singleton(局部静态)是最简洁、性能最佳的实现,但需注意 C++11 之前的线程安全问题。
  • 双重检查锁 需要谨慎处理可见性和重排序,适用于旧标准下的手动实现。
  • std::call_once 兼顾性能与显式锁定,适合需要动态初始化的情况。
  • C++17 的 inline static 进一步简化了实现,保证跨模块的一致性。

根据项目需求、编译器支持和线程安全等级,选择合适的实现方式即可。

C++ 中的智能指针:std::unique_ptr 与 std::shared_ptr 的区别及最佳实践

在 C++11 及以后,智能指针已成为资源管理的核心工具。本文聚焦两大主流实现——std::unique_ptrstd::shared_ptr,从语义、性能、安全性等维度对比它们,并给出在实际项目中的最佳实践。

1. 语义区别

  • std::unique_ptr
    • 所有权独占:指针拥有唯一所有者,无法复制,只能移动。
    • 自动释放:所有者销毁时自动调用删除器,适合单一对象或链表、树等结构。
  • std::shared_ptr
    • 共享所有权:可以多处复制,内部维护引用计数。计数为零时销毁。
    • 多线程安全:引用计数自增/自减是原子操作,适合共享资源。

2. 性能考量

特性 unique_ptr shared_ptr
额外开销 0(仅指针) 2 个引用计数(一个强计数、一个弱计数)
线程开销 原子计数操作,轻量但仍有开销
内存占用 8/16 B(64/32 bit) 24/32 B(64/32 bit)+ 管理块

当不需要共享所有权时,unique_ptr 是首选,既安全又高效。

3. 错误使用场景

  • 误用 shared_ptr 造成循环引用

    struct Node {
        std::shared_ptr <Node> next;
        std::weak_ptr <Node> prev; // 必须弱引用
    };

    prev 也是 shared_ptr,两个节点会互相持有,导致内存泄漏。

  • 在多线程里使用 unique_ptr 共享数据
    unique_ptr 不是线程安全的,若跨线程使用必须使用 std::shared_ptr 或外部同步。

  • 与 C API 交互时忘记自定义删除器

    std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(...), fclose);

    unique_ptr 的删除器默认是 delete,不适用于 C 资源。

4. 最佳实践

  1. 优先使用 unique_ptr

    • 对象生命周期由创建者决定,避免不必要的引用计数。
    • 通过 std::move 将所有权转移给容器或返回值。
  2. 在需要共享所有权时才使用 shared_ptr

    • 例如 GUI 事件回调、缓存共享、资源池。
    • 如果可能,使用 std::weak_ptr 防止循环引用。
  3. 自定义删除器

    • 对于自定义资源或 C 风格对象,传入删除器或使用 std::unique_ptr<... , Deleter>
  4. 与 std::make_unique / std::make_shared 结合使用

    • 避免多次 new,保证异常安全。
  5. 考虑 move-only 对象

    • std::vector<std::unique_ptr<T>> 需要使用 emplace_back(std::make_unique<T>(...))
    • std::vector<std::shared_ptr<T>> 可以直接 push_back

5. 代码示例

5.1 资源管理

// 文件句柄管理
class File {
    std::unique_ptr<FILE, decltype(&fclose)> file_;
public:
    explicit File(const char* path) :
        file_(fopen(path, "r"), fclose) {
        if (!file_) throw std::runtime_error("open failed");
    }
    FILE* get() const { return file_.get(); }
};

5.2 共享资源示例

class Widget {
public:
    Widget() { std::cout << "Widget created\n"; }
    ~Widget() { std::cout << "Widget destroyed\n"; }
};

void worker(std::shared_ptr <Widget> w) {
    std::cout << "Worker use widget\n";
}

int main() {
    auto w = std::make_shared <Widget>();
    std::thread t1(worker, w);
    std::thread t2(worker, w);
    t1.join(); t2.join(); // 共享所有权,引用计数自动管理
}

5.3 防止循环引用

struct Node {
    std::shared_ptr <Node> next;
    std::weak_ptr <Node> prev;  // 关键:使用 weak_ptr
};

void link(Node* a, Node* b) {
    a->next = std::shared_ptr <Node>(b);
    b->prev = std::weak_ptr <Node>(a);
}

6. 结语

在 C++ 编程中,智能指针的正确使用是提高代码安全性和可维护性的关键。

  • unique_ptr 适合拥有唯一所有者的资源,既简单又高效。
  • shared_ptr 在需要共享所有权时才使用,并配合 weak_ptr 防止循环引用。

遵循上述最佳实践,可在保持代码简洁的同时,避免内存泄漏和资源竞争问题。

如何在C++中实现自定义共享指针(Smart Pointer)

在现代 C++ 开发中,智能指针是管理动态内存的重要工具。标准库中已经提供了 std::shared_ptrstd::unique_ptr 等实现,但在一些特定场景下,你可能需要一个更轻量或更具可定制性的共享指针。下面将展示一个最小化、易于理解的自定义 SharedPtr 实现,并讨论其在多线程环境中的使用注意事项。

1. 设计思路

  • 引用计数:核心是一个共享计数器,记录指向同一资源的 SharedPtr 对象数量。
  • 原子操作:为了线程安全,计数器使用 `std::atomic `。
  • 自定义删除器:支持用户传入自定义的析构函数,以兼容多态或非 new 分配的资源。
  • 基本接口:实现 operator*, operator->, get(), use_count(), reset(), swap() 等常用成员。

2. 代码实现

#include <atomic>
#include <utility>
#include <iostream>
#include <cassert>

template<typename T>
class SharedPtr {
public:
    // 默认构造,指向空
    SharedPtr() noexcept : ptr_(nullptr), count_(nullptr), deleter_(nullptr) {}

    // 从裸指针创建
    explicit SharedPtr(T* p, std::function<void(T*)> deleter = std::default_delete<T>())
        : ptr_(p), deleter_(std::move(deleter)) {
        count_ = new std::atomic <size_t>(1);
    }

    // 拷贝构造
    SharedPtr(const SharedPtr& other) noexcept
        : ptr_(other.ptr_), count_(other.count_), deleter_(other.deleter_) {
        if (count_) ++(*count_);
    }

    // 移动构造
    SharedPtr(SharedPtr&& other) noexcept
        : ptr_(other.ptr_), count_(other.count_), deleter_(std::move(other.deleter_)) {
        other.ptr_ = nullptr;
        other.count_ = nullptr;
    }

    // 拷贝赋值
    SharedPtr& operator=(const SharedPtr& other) {
        if (this != &other) {
            release();                       // 先释放自身资源
            ptr_ = other.ptr_;
            count_ = other.count_;
            deleter_ = other.deleter_;
            if (count_) ++(*count_);
        }
        return *this;
    }

    // 移动赋值
    SharedPtr& operator=(SharedPtr&& other) noexcept {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            count_ = other.count_;
            deleter_ = std::move(other.deleter_);
            other.ptr_ = nullptr;
            other.count_ = nullptr;
        }
        return *this;
    }

    // 析构
    ~SharedPtr() {
        release();
    }

    // 解引用
    T& operator*() const { assert(ptr_); return *ptr_; }
    T* operator->() const { assert(ptr_); return ptr_; }
    T* get() const noexcept { return ptr_; }

    size_t use_count() const noexcept { return count_ ? *count_ : 0; }

    void reset() noexcept {
        release();
        ptr_ = nullptr;
        count_ = nullptr;
        deleter_ = nullptr;
    }

    void reset(T* p) {
        if (ptr_ != p) {
            release();
            ptr_ = p;
            deleter_ = std::default_delete <T>();
            count_ = new std::atomic <size_t>(1);
        }
    }

    void swap(SharedPtr& other) noexcept {
        std::swap(ptr_, other.ptr_);
        std::swap(count_, other.count_);
        std::swap(deleter_, other.deleter_);
    }

    explicit operator bool() const noexcept { return ptr_ != nullptr; }

private:
    void release() {
        if (count_ && --(*count_) == 0) {
            if (deleter_) deleter_(ptr_);
            delete count_;
        }
    }

    T* ptr_;
    std::atomic <size_t>* count_;
    std::function<void(T*)> deleter_;
};

3. 使用示例

struct Base { virtual void foo() { std::cout << "Base\n"; } };
struct Derived : Base { void foo() override { std::cout << "Derived\n"; } };

int main() {
    SharedPtr <Base> sp1(new Derived());           // 计数为1
    {
        SharedPtr <Base> sp2 = sp1;                // 计数变为2
        std::cout << sp2.use_count() << "\n";     // 输出2
        sp2->foo();                               // 调用 Derived::foo
    } // sp2析构,计数变为1
    std::cout << sp1.use_count() << "\n";         // 输出1

    // 自定义删除器(例如使用 `malloc` 分配)
    SharedPtr <int> sp3(reinterpret_cast<int*>(std::malloc(sizeof(int))),
                       [](int* p){ std::free(p); });
    *sp3 = 42;
    std::cout << *sp3 << "\n";                    // 输出42

    return 0;
}

4. 多线程注意事项

  • 计数器使用 std::atomic,实现了基本的线程安全计数增减。
  • 对同一对象的并发访问(读写)仍需外部同步(如 std::mutex),因为 SharedPtr 本身不提供数据层面的同步。
  • reset()release() 的实现是原子操作,但在高并发场景下,频繁的 reset() 可能导致大量内存分配/释放。可考虑预先分配计数器或使用内存池。

5. 与 std::shared_ptr 的比较

特性 std::shared_ptr 自定义 SharedPtr
线程安全计数
兼容性 与 STL 容器、算法完美配合 需要自行实现容器友好
性能 内置优化(如 make_shared 可能更轻量,但缺少优化
删除器 支持自定义 支持自定义
其他 友好异常安全、weak_ptr 支持 仅实现核心功能

6. 结语

自定义共享指针可以帮助你在特殊需求下精细控制内存管理、减少不必要的开销或满足教学需求。但在大多数生产环境中,建议优先使用标准库提供的 std::shared_ptr。如果你确实需要定制行为,以上实现已经具备基本的可用性,后续可以根据项目需求进一步扩展。

C++17 中的 std::filesystem:文件系统操作的新利器

随着 C++17 的正式发布,STL 中引入了 std::filesystem 库,彻底改变了我们在 C++ 程序中处理文件和目录的方式。传统上,开发者往往需要依赖平台特定的 API(如 Windows API 的 Win32 或 POSIX 的 ),或使用第三方库(Boost.FileSystem、libuv 等)来完成文件路径拼接、目录遍历、文件拷贝等操作。std::filesystem 的出现,为所有符合标准的编译器提供了统一、跨平台、类型安全的接口,极大地方便了文件系统编程。

1. 核心概念

1.1 path

std::filesystem::path 是对文件路径的抽象,内部采用 std::stringstd::wstring 存储,提供了丰富的操作接口,例如:

std::filesystem::path p = "/usr/local/bin";
p /= "cpp";
p += ".exe";

使用 operator/= 可以自动处理路径分隔符,无论是 Windows 的 \ 还是 POSIX 的 /

1.2 directory_iterator / recursive_directory_iterator

  • directory_iterator 用于一次性遍历指定目录下的文件与子目录。
  • recursive_directory_iterator 递归遍历整个目录树。

示例:

for (const auto& entry : std::filesystem::directory_iterator("/tmp"))
    std::cout << entry.path() << '\n';

1.3 file_status 与 file_type

使用 status()symlink_status() 可以获取文件的属性,如大小、类型、权限等。

auto ftype = std::filesystem::file_type::regular_file;
if (std::filesystem::status(p).type() == ftype) { /* ... */ }

2. 常见操作示例

2.1 检查文件是否存在

if (std::filesystem::exists("data.txt")) {
    std::cout << "文件存在\n";
}

2.2 获取文件大小

std::uintmax_t sz = std::filesystem::file_size("data.txt");
std::cout << "大小: " << sz << " 字节\n";

2.3 复制、移动、删除文件

std::filesystem::copy_file(src, dst, std::filesystem::copy_options::overwrite_existing);
std::filesystem::rename(src, dst); // 移动
std::filesystem::remove(path);     // 删除单文件
std::filesystem::remove_all(dir);  // 删除目录及其所有内容

2.4 创建和删除目录

std::filesystem::create_directories("/tmp/foo/bar/baz"); // 多级目录
std::filesystem::remove_all("/tmp/foo");                 // 删除整棵树

3. 线程安全与异常处理

std::filesystem 所有操作都是线程安全的,但在并发读写同一文件时需要自行同步。异常方面,所有函数都可能抛出 std::filesystem::filesystem_error,可通过捕获该异常获取详细错误信息:

try {
    std::filesystem::remove("nonexistent.txt");
} catch (const std::filesystem::filesystem_error& e) {
    std::cerr << e.what() << '\n';
}

4. 性能注意

  • 延迟加载directory_iterator 在迭代时才查询文件系统,避免一次性读取大量目录。
  • 缓存status() 可能触发磁盘访问,频繁调用会导致性能下降,建议使用 symlink_status() 或自行缓存结果。
  • IO 调度:对大文件读写时,使用 std::ofstream / std::ifstream 配合 std::filesystem::copy_file 更快。

5. 与 Boost.FileSystem 的对比

Boost.FileSystem 是 std::filesystem 的前身,功能几乎相同,但存在以下差异:

维度 std::filesystem Boost.FileSystem
标准化 是 C++17 标准 非标准
头文件
类型安全 高(使用 path 类型) 低(多使用字符串)
依赖 Boost 依赖
线程安全 是(但实现细节不同)

若项目已迁移到 C++17 或更高版本,建议直接使用 std::filesystem,除非需要保持向后兼容。

6. 小结

std::filesystem 为 C++ 提供了完整、跨平台、类型安全的文件系统接口,极大简化了文件操作代码。熟练掌握其核心类和函数后,许多日常任务(路径拼接、目录遍历、文件复制等)将变得毫不费力。未来的 C++ 标准会继续扩展该库,例如在 C++20 中增加了 temp_directory_path()current_path() 的改进,以及在 C++23 中对异常类型和错误码的进一步完善。掌握 std::filesystem 是现代 C++ 开发者不可或缺的技能。

如何使用 C++17 的 std::variant 进行类型安全的多态处理

在 C++17 标准中,std::variant 被引入来解决传统多态(如 void*std::any、以及类继承层次结构)所带来的类型不安全与性能损失。本文将详细介绍 std::variant 的基本使用、访问方式、遍历访问、与其他 STL 容器结合使用,以及如何借助 std::visit 实现类型安全的多态函数。


1. std::variant 简介

std::variant 是一个可存放多种类型的容器,只能存放 一种 类型的值。它相当于一个“类型安全的” std::union,但提供了更强的类型检查和更丰富的操作接口。

std::variant<int, double, std::string> var;
var = 42;                  // 存储 int
var = 3.14;                // 覆盖为 double
var = std::string("hello"); // 覆盖为 std::string

若想存放自定义类型,必须确保该类型满足 std::variant 的要求:可移动、可拷贝、可比较(可选)等。


2. 访问和检查当前存储类型

2.1 std::get / std::get_if

int i = std::get <int>(var);            // 如果 var 当前不是 int,会抛 std::bad_variant_access
int* p = std::get_if <int>(&var);       // 若是 int,返回指针,否则返回 nullptr

2.2 std::holds_alternative

if (std::holds_alternative <double>(var)) {
    // ...
}

2.3 index()type()

size_t idx = var.index();                 // 当前类型的索引,0 表示第一个类型
const std::type_info& ti = var.type();    // 当前类型的 type_info

3. std::visit:访问者模式的现代实现

std::visit 接受一个可调用对象(如 lambda、函数对象、函数指针)和一个或多个 variant,根据 variant 当前存储的类型调用对应的重载版本。

auto visitor = [](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "int: " << arg << '\n';
    } else if constexpr (std::is_same_v<T, double>) {
        std::cout << "double: " << arg << '\n';
    } else {
        std::cout << "string: " << arg << '\n';
    }
};

std::visit(visitor, var);

如果传入多个 variant,std::visit 会使用 参数包展开 并生成对应组合的重载。

std::variant<int, std::string> a = 10;
std::variant<double, std::string> b = "test";

std::visit([](auto&& va, auto&& vb) {
    std::cout << va << " | " << vb << '\n';
}, a, b);

4. 与其他 STL 容器结合

4.1 std::vector<std::variant<...>>

std::vector<std::variant<int, std::string>> vec;
vec.push_back(42);
vec.push_back("hello");

遍历时可使用 std::visitstd::get_if

4.2 递归结构

自定义递归类型时,需要使用 std::variant 包装指针或 std::shared_ptr

struct Expr;
using ExprPtr = std::shared_ptr <Expr>;

struct Const { int value; };
struct Add { ExprPtr left, right; };

struct Expr : std::variant<Const, Add> {
    using variant::variant;
};

这样可以避免无限递归类型定义。


5. 性能考虑

  • std::variant 的内部实现类似于 union + index。访问时不需要虚函数表,避免了动态分派的开销。
  • std::visit 的调用会被编译器优化为 switchif-else,并可使用模板递归展开,生成高效代码。
  • std::any 相比,std::variant 的类型检查是 编译时 的,能在编译期捕获错误。

6. 常见陷阱

  1. 缺失类型
    若在 variant 中未包含所需的类型,编译器会报错。

    std::variant <int> v;
    v = 3.14; // 错误
  2. 未捕获 bad_variant_access
    使用 std::get 访问错误类型会抛异常。

    try { std::get <double>(v); }
    catch (const std::bad_variant_access& e) { /* 处理 */ }
  3. 递归结构导致堆栈溢出
    如前所述,需要使用指针包装递归类型。


7. 典型应用场景

场景 传统实现 std::variant 实现 优点
消息系统 基于 enum + union + 手动 switch variant + visit 更安全、易扩展
解析树 多个类继承自基类 递归 variant 消除虚表,降低内存占用
配置文件 std::map<std::string, boost::variant> std::variant 省去第三方库,标准化
UI 事件 std::variant std::variant 统一事件类型,避免类型转换

8. 小结

std::variant 为 C++ 提供了 类型安全的代替 union,与 std::visit 的配合实现了 现代多态 的高效方案。相比传统的 void*std::any 或继承层次结构,variant 在类型检查、性能和易用性上都有显著提升。通过本文的示例与技巧,相信读者已经具备使用 std::variant 进行安全多态编程的基本能力。


**C++20 中的 std::span 的使用场景与实现细节**

std::span 是 C++20 新增的一个非常实用的轻量级视图容器,它为我们提供了一种安全、高效地访问数组、std::vectorstd::array 或任何连续内存块的方式,而无需拷贝。下面从使用场景、语义、实现细节以及常见陷阱等角度展开讨论。


一、使用场景

  1. 函数参数传递
    当你需要让一个函数处理任意长度的数据时,传统的做法是使用指针+长度或者引用+长度。std::span 把这两种信息打包成一个对象,接口更加简洁且更安全。

    void process(const std::span <int> data) {
        for (auto v : data) { /* 处理 */ }
    }
  2. 与 STL 容器互操作
    std::span 可以直接从 std::vectorstd::array 或 C 数组构造,且可以与 STL 算法一起使用。

    std::vector <int> vec = {1,2,3,4,5};
    process(std::span <int>{vec});          // 直接传递 vector
    process(std::span <int>{vec.data(), 3}); // 只处理前三个元素
  3. 多维数组的行/列视图
    通过 std::span<std::span<int>> 可以对二维数组的行进行操作,或将二维数组平铺为一维视图进行快速遍历。

  4. 与内存映射文件(mmap)配合
    在系统编程中,std::span 可直接映射到文件内容,提供安全的读写接口。


二、语义与安全

  • 只读/可变:`std::span ` 是可变的,`std::span` 是只读的。通过 const 修饰可以避免意外修改。
  • 生命周期std::span 本身不拥有底层数据,它仅仅是对外部内存的视图。因此,使用者必须确保底层数据在 span 生命周期内保持有效。最常见的错误是把指向局部数组的 span 返回给调用者。
  • 不可为空std::span 允许长度为 0,但内部指针必须合法(通常指向有效内存)。如果底层数据为空,构造时需要传递 nullptr 与长度 0。

三、实现细节

template<class T>
class span {
    T*  _ptr;   // 指向第一个元素
    std::size_t _len; // 元素数量

public:
    constexpr span() noexcept : _ptr(nullptr), _len(0) {}
    constexpr span(T* ptr, std::size_t len) noexcept : _ptr(ptr), _len(len) {}

    // 兼容 std::vector, std::array, C-array
    template<class Container>
    constexpr span(Container& c) noexcept
        : _ptr(c.data()), _len(c.size()) {}

    // 兼容二维视图
    template<class U>
    constexpr span(std::span <U> s) noexcept
        : _ptr(reinterpret_cast<T*>(s.data())), _len(s.size()) {}

    constexpr T* data() const noexcept { return _ptr; }
    constexpr std::size_t size() const noexcept { return _len; }
    constexpr T& operator[](std::size_t i) const { return _ptr[i]; }
    constexpr bool empty() const noexcept { return _len == 0; }

    // 子span
    constexpr span subspan(std::size_t pos, std::size_t n = std::dynamic_extent) const noexcept {
        if (pos > _len) throw std::out_of_range("span subspan");
        if (n == std::dynamic_extent) n = _len - pos;
        if (pos + n > _len) throw std::out_of_range("span subspan");
        return span(_ptr + pos, n);
    }
};
  • std::dynamic_extent 是一个特殊的常量,表示在子视图中没有指定长度,默认使用剩余元素。
  • span 通常是一个 POD(Plain Old Data)结构,具有极低的构造成本。现代编译器在内联展开后,几乎不产生任何运行时开销。

四、常见陷阱与最佳实践

场景 问题 解决方案
返回 span 返回指向局部数组的 span 1. 返回 `std::span
只在外部数据有效时 2. 采用std::vectorstd::array并返回std::span`
共享内存 多线程同时写同一 span 使用同步原语(如 std::mutex)或只读 span<const T>
变长视图 std::span 的长度是固定的 在需要变长时使用 std::vectorstd::stringspan 只做临时视图
访问越界 子视图长度错误 subspan 会检查越界并抛出异常;在生产环境中可使用 std::size_t 预检查

五、实战案例:实现一个泛型排序函数

#include <algorithm>
#include <span>
#include <iostream>

template<class T, class Compare = std::less<T>>
void generic_sort(std::span <T> s, Compare comp = Compare{}) {
    std::sort(s.begin(), s.end(), comp);
}

int main() {
    std::vector <int> vec = {4, 1, 3, 2};
    generic_sort(vec);           // 直接传 vector
    generic_sort(std::span <int>{vec.data(), 2}); // 只排序前两个元素
    for (auto v : vec) std::cout << v << ' '; // 输出 1 4 3 2
}

此例中 generic_sort 不关心底层容器,所有只需满足 begin()end() 的容器都能通过 std::span 适配。


六、结语

std::span 的出现极大简化了对连续内存块的访问,提升了代码安全性和可读性。掌握它的使用原则、生命周期管理与常见陷阱,能让你的 C++20 代码更简洁、更高效。若想进一步提升性能,可配合 std::as_conststd::span<const T> 进行只读访问,减少不必要的写操作。希望本文能帮助你在日常开发中灵活运用 std::span

从 C++20 到 C++23:模块化编程的全新生态

随着 C++ 标准不断演进,C++20 已经为我们提供了模块化编程、概念、协程、范围等一系列强大特性,而 C++23 在此基础上进一步完善了模块化系统、引入了更细粒度的概念改进,并改进了编译器与工具链的交互方式。本文将带你快速了解 C++23 在模块化编程方面的主要升级,并给出一些实用示例,帮助你在项目中快速落地。

1. 模块化编程的进化路径

版本 关键改进 影响
C++20 引入模块,取代传统头文件 减少重编译时间,提升编译速度
C++23 改进模块导出方式、引入模块属性、支持更细粒度的命名空间导出 提升模块可维护性,兼容更复杂的编译体系

1.1 传统头文件的痛点

  • 重复编译:每个文件包含同一头文件时,编译器都会重新编译。
  • 头文件污染:全局宏、using namespace 可能导致名称冲突。
  • 编译器之间不一致:不同编译器对 pragma once 的支持程度不同。

1.2 C++20 模块的解决方案

  • 模块导入(import):取代 #include,只在编译单元首次导入时编译一次。
  • 模块单元(module unit):将相关实现拆分成逻辑单元,保持代码组织。
  • 私有导出export module mylibexport 关键字配合,控制可见性。

2. C++23 对模块化的进一步优化

2.1 export module 的细粒度控制

C++23 允许在模块内部使用 export 关键字对单独的实体(函数、类、变量)进行导出,而不必一次性导出整个模块单元。例如:

module; // 普通预处理器指令
export module geometry;

namespace geom {
    export struct Point { double x, y; };
    export double distance(Point const& a, Point const& b);
}

这使得模块内部实现可以保持私有,而只暴露必要接口。

2.2 模块属性 export 的改进

  • export import:可以在模块外部使用 export import 将导出的实体直接导入到全局命名空间,类似于 using namespace,但更安全、更明确。
  • export module 选项:支持 export module name { attribute },如 export module math { export } 用于声明模块具有 export 访问权限。

2.3 与编译器的协同

  • 更友好的编译缓存:支持基于模块的增量编译缓存,编译器只需要重新编译受更改影响的模块单元。
  • 统一的模块文件格式.ifc(Intermediate Frontend Container)文件可以跨编译器使用,避免二进制兼容问题。

3. 实际案例:从头文件到模块化的迁移

3.1 原始头文件版本

// math.h
#pragma once
#include <cmath>

namespace math {
    inline double square(double x) { return x * x; }
    inline double sqrt(double x) { return std::sqrt(x); }
}

3.2 迁移为 C++20 模块

// math.ixx
export module math;

export namespace math {
    export inline double square(double x) { return x * x; }
    export inline double sqrt(double x) { return std::sqrt(x); }
}

3.3 在项目中使用

// main.cpp
import math;

int main() {
    double a = math::square(3.0);
    double b = math::sqrt(16.0);
}

编译命令(GCC 13)

g++ -std=c++20 -c math.ixx -o math.o
g++ -std=c++20 -c main.cpp -o main.o
g++ math.o main.o -o app

4. 未来展望

  • 更丰富的模块导入语法:如 `import as alias`,提升代码可读性。
  • 跨平台模块化支持:移动到 LLVM IR 层,进一步提升编译速度。
  • 标准化的模块缓存:类似 Python 的 .pyc,为 C++ 模块提供二进制缓存文件,减少编译时间。

5. 小结

C++23 在模块化编程方面做了细致且实用的改进,使得模块的可维护性、可读性和编译效率得到进一步提升。对已有项目进行模块化迁移,既能显著提升编译性能,也能降低命名冲突和头文件污染的风险。随着编译器对模块化支持的进一步完善,C++ 的模块化编程将成为大规模项目的标准实践。