C++17 中的 std::variant 与 std::visit:实现多态的现代方式

在 C++ 17 之前,开发者常用 std::variant 来实现类型安全的联合体,结合 std::visit 则能像使用虚函数一样处理不同类型的数据。相比传统的继承和 dynamic_cast,这种方式更安全、可维护性更高。本文将从基本使用、性能比较、以及高级用法三方面,详细介绍如何在实际项目中应用 std::variantstd::visit

1. 基础语法

#include <variant>
#include <iostream>
#include <string>

using Value = std::variant<int, double, std::string>;

void printValue(const Value& v) {
    std::visit([](auto&& arg) {
        std::cout << arg << std::endl;
    }, v);
}

int main() {
    Value v1 = 42;
    Value v2 = 3.14;
    Value v3 = std::string("Hello, C++17!");

    printValue(v1);
    printValue(v2);
    printValue(v3);
}

上述代码中,Value 可以保存三种类型的值。printValue 使用 std::visit 调用一个 lambda,lambda 的模板参数 auto&& 能匹配任何类型。编译器在运行时会根据当前存储的类型调用相应的 lambda 分支。

2. 类型安全的访问

与传统的 boost::variant 或手写的联合体不同,std::variant 在编译时就能确保类型安全。若想访问当前存储的类型,可以使用 `std::get

` 或 `std::get_if`: “`cpp int main() { Value v = 100; if (auto p = std::get_if (&v)) { std::cout ` 会抛出 `std::bad_variant_access`,而 `std::get_if` 则返回空指针,避免异常。 ## 3. 性能对比 传统多态实现(虚表 + RTTI)在某些场景下会产生一定的运行时开销。`std::variant` 与 `std::visit` 的实现是基于 `switch` 或 `constexpr if`,在编译期就能生成对应分支,减少了运行时判断。 – **内存占用**:`std::variant` 的内存占用是所有成员类型中最大的类型大小加上一个小的标识符(通常是 `size_t`)。与虚表大小相近,但不需要每个对象都持有指向虚表的指针。 – **调用成本**:`std::visit` 通过模板展开在编译期生成调用,类似直接调用函数指针,几乎不比虚拟函数调用慢。 ## 4. 结合结构体与 std::variant 有时我们需要一个结构体中包含多个可能的子对象,而子对象又可能是多态的。此时可以将 `std::variant` 嵌套在结构体内部: “`cpp struct Shape { std::variant shape_; }; void area(const Shape& s) { std::visit([](auto&& sp) { std::cout ; OptInt opt = std::monostate{}; if (std::holds_alternative(opt)) { std::cout ` 更轻量,且不需要额外的 `optional` 包装。 ## 6. 与 std::any 的区别 – **std::any**:存储任意类型,但在访问时需要显式指定类型,且不提供编译时的类型安全检查。适合“任意数据”而不关心类型。 – **std::variant**:存储有限且已知类型集合,提供编译时类型安全和 `visit` 机制,适合需要多态但类型已知的场景。 ## 7. 典型应用场景 1. **解析 JSON / YAML**:可以将值类型定义为 `std::variant, std::map>`,方便递归解析。 2. **状态机**:每个状态可以用不同的结构体表示,使用 `std::variant` 保存当前状态,并通过 `std::visit` 处理事件。 3. **日志系统**:日志条目可以包含多种数据类型,使用 `std::variant` 简化日志格式处理。 ## 8. 小结 `std::variant` 与 `std::visit` 为 C++ 提供了一个类型安全、性能优异的多态实现方案。相比传统的继承+RTTI,能够在编译期捕获错误,减少运行时成本。通过合理的设计,将它们与结构体、容器等结合,可在项目中实现清晰且易维护的代码结构。 在实际编码中,建议先列举业务可能出现的所有类型,然后用 `std::variant` 包装。这样既能保持类型安全,又能避免不必要的 RTTI 开销。

**C++17 中的 constexpr if 的使用与优化**

在 C++17 中,constexpr if 成为模板元编程的强大工具。它允许编译器在编译阶段根据条件判断来选择代码路径,从而实现更高效、可读性更好的模板代码。以下内容将从语法、典型用例、性能优化以及与 SFINAE 的对比等角度详细阐述 constexpr if 的使用。

1. 语法基础

template<typename T>
void foo(const T& value) {
    if constexpr (std::is_integral_v <T>) {
        // 仅当 T 为整数类型时编译此分支
        std::cout << "Integral: " << value << '\n';
    } else {
        // 仅当 T 不是整数类型时编译此分支
        std::cout << "Non-integral: " << value << '\n';
    }
}
  • if constexpr 必须是 if 语句。
  • 条件表达式必须在编译时求值为常量表达式。
  • if constexprtrue 分支和 false 分支在 不满足条件 的那一侧 不会被编译。这使得我们可以在 true 分支中使用仅适用于特定类型的成员,而无需担心编译错误。

2. 与 SFINAE 的对比

传统上,模板特化或 SFINAE(Substitution Failure Is Not An Error)用于实现条件编译。但这些方法往往导致代码冗长、可读性差。constexpr if 的优势:

方案 代码行数 读写复杂度 生成二进制大小 错误定位
SFINAE 12+ 1.2×
constexpr if 6 1.0×

3. 常见使用场景

3.1 条件日志输出

template<typename Logger>
void logValue(const Logger& logger, const auto& value) {
    if constexpr (std::is_same_v<decltype(logger.level()), int>) {
        if (logger.level() > 2) {
            logger.write(value);
        }
    } else { // 假设 logger.level() 返回 std::string
        if (logger.level() == "DEBUG") {
            logger.write(value);
        }
    }
}

3.2 对容器进行遍历的优化

template<typename Container>
void processContainer(const Container& c) {
    if constexpr (std::ranges::random_access_range <Container>) {
        // 随机访问容器可以使用索引遍历
        for (size_t i = 0; i < c.size(); ++i)
            handle(c[i]);
    } else {
        // 迭代器遍历
        for (const auto& item : c)
            handle(item);
    }
}

4. 性能优化技巧

  1. 避免多层嵌套:虽然 if constexpr 可以嵌套,但过深的嵌套会导致编译时间增长。建议将逻辑拆分为辅助函数。
  2. 使用 std::conditional_t 替代:在需要根据类型选择类型别名时,std::conditional_tif constexpr 可以配合使用,进一步减少代码冗余。
  3. std::is_constant_evaluated() 配合:在 constexpr 函数内部,结合 std::is_constant_evaluated() 判断当前是否在编译时求值,以提供不同实现。
template<typename T>
constexpr T maxVal(const T& a, const T& b) {
    if (std::is_constant_evaluated()) {
        return a > b ? a : b; // 编译时求值
    } else {
        // 运行时逻辑(可能包含日志或额外检查)
        return a > b ? a : b;
    }
}

5. 实际项目案例

在某开源图形库中,作者使用 constexpr if 对不同渲染后端(OpenGL、Vulkan、DirectX)进行分支,避免了多重模板特化和宏定义。结果是:

  • 编译时间下降了 15%。
  • 代码量减少约 30%。
  • 对新后端的扩展仅需添加对应的实现函数。

6. 结语

constexpr if 已经成为 C++17 的重要特性之一,它让模板元编程既简洁又高效。通过合理地把握其使用场景与优化技巧,开发者可以在不牺牲性能的前提下,写出更易维护、可读性更强的代码。随着 C++20、C++23 等新标准的到来,constexpr if 的应用场景将进一步扩展,为高级模板编程打开更多可能。

## C++20 模块:一次性把头文件引入的革命性改变

在过去的几十年里,C++ 通过预编译头文件(PCH)和传统的 #include 指令来实现代码共享。然而这些方法存在编译时间长、依赖管理复杂、冲突多等痛点。C++20 标准正式引入了 模块(Modules) 概念,打破了传统头文件机制,提供了一种更高效、更安全、更可维护的代码共享方式。本文将从概念、实现、编译过程以及使用技巧四个角度,全面剖析 C++20 模块。


1. 传统头文件的痛点

痛点 说明
编译时间长 每个翻译单元都需要解析相同的头文件,导致重复工作。
宏污染 宏定义往往在整个项目中可见,容易产生冲突。
依赖关系难以管理 #include 形成的隐式依赖,无法直观查看。
编译错误难定位 错误可能发生在被包含文件内部,定位困难。

这些痛点在大型项目中尤为突出,迫切需要更好的解决方案。


2. 模块的基本概念

C++20 模块由 模块单元(module unit)导出(export)使用(import) 三个核心概念构成:

  • 模块单元:相当于一个独立的编译单元,包含所有源文件和头文件,并且可以被编译为二进制模块文件(.pcm.ixx 等)。
  • 导出(export):声明哪些符号(类、函数、变量等)对外可见。仅导出的符号才会被其他模块使用。
  • 使用(import):在其他翻译单元或模块中引入已导出的符号,类似传统的 #include,但不会把源代码拷贝进去。

3. 编译流程对比

步骤 传统头文件 C++20 模块
1 对每个翻译单元逐行解析 #include 先编译模块单元生成二进制模块文件
2 每个翻译单元都解析相同头文件 只解析一次模块单元,生成 .pcm
3 编译器读取并展开宏定义 模块内部已解析宏,外部无宏污染
4 链接时所有符号全部展开 链接时仅引用已导出的符号,避免冲突

通过以上对比可以看出,模块显著减少了编译时间并提升了代码可维护性。


4. 模块的实现细节

4.1 模块声明文件(.ixx.cppm

module MyMath;          // 模块名

export module;          // 必须的语法,分隔模块内部与导出区域

export namespace math {
    export double add(double a, double b);
    export double sub(double a, double b);
}
  • module MyMath; 定义模块名,后续所有 import MyMath; 都会引用它。
  • export 关键字放在需要导出的符号前。

4.2 模块实现文件(.cppm.ixx

module MyMath; // 仍需声明模块名

namespace math {
    double add(double a, double b) { return a + b; }
    double sub(double a, double b) { return a - b; }
}

实现文件不需要再次 export,因为它们在同一模块内部。

4.3 生成模块接口文件

在编译时使用 -fmodules-ts-fmodules(视编译器而定):

g++ -std=c++20 -fmodules-ts -c math.cppm -o math.pcm

得到 math.pcm,随后可以在其他文件中直接 import

4.4 使用模块

import MyMath; // 导入模块

int main() {
    double x = math::add(3.5, 2.5);
    std::cout << x << std::endl;
}

编译:

g++ -std=c++20 -fmodules-ts main.cpp math.pcm -o app

5. 模块使用技巧

  1. 将公共头文件拆成模块
    std::vectorstd::string 等常用 STL 头文件提前编译为模块,避免每个文件都包含一次。

  2. 使用隐式模块导入
    对于标准库,编译器已经提供了 module std,直接 import std; 即可使用所有标准符号,减少头文件数量。

  3. 避免循环依赖
    与传统头文件类似,模块也需要避免循环导入。使用 export moduleexport import 的分离原则,可以清晰地控制依赖关系。

  4. 与旧代码混合
    C++20 模块可以与传统头文件共存。只要在项目构建系统中为需要使用模块的文件开启 -fmodules-ts,其他文件保持旧模式即可。

  5. 构建系统配置

    • CMake:使用 target_sources + target_link_optionstarget_precompile_headers
    • Bazel:支持 cc_library + modules 属性
    • Makefile:需要手动管理 .pcm 生成与链接

6. 未来展望

  • 更好的 IDE 支持:编辑器将直接读取 .pcm,实现智能补全、跳转等功能。
  • 跨平台模块缓存:利用二进制模块文件可以共享编译缓存,减少多平台构建成本。
  • 增强模块隔离:结合模块接口分隔符(export module)和 module 内的 import 控制,进一步提升代码安全。

结语

C++20 模块是对传统头文件机制的一次革命,它通过二进制模块文件显著降低编译时间、提升代码可维护性,并为大型项目提供了更清晰的依赖管理方案。虽然还需要工具链与 IDE 的完善支持,但未来的 C++ 开发者一定会受益匪浅。接下来,你可以尝试将现有项目的一部分迁移为模块,亲身体验这场变革带来的效率提升。

C++20中协程的使用与实现细节

在C++20中,协程(coroutines)被正式纳入语言标准,提供了一种更高层次的异步编程模型。相比传统的回调或Future/Promise,协程让代码更像同步流程,更易读、维护。本文将从语法、实现原理、典型使用场景以及常见坑点四个角度,系统地剖析C++20协程的核心概念与实践。

1. 协程基本语法

1.1 co_awaitco_yieldco_return

  • co_await:等待一个协程或异步操作完成,类似await
  • co_yield:生成一个值给消费者,类似生成器。
  • co_return:结束协程,返回最终结果。

1.2 协程函数的返回类型

C++20将协程函数的返回类型分为三类:

  • `std::future ` / `std::shared_future`(标准库实现)
  • `std::generator `(C++23提供)
  • 自定义 `awaitable ` 或 `generator`(常见于Boost.Asio等)

编译器会自动插入一个“promise”对象,协程函数的状态机由此生成。

1.3 示例代码

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

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

Task simple() {
    std::cout << "Before co_await\n";
    co_await std::suspend_always{};   // 模拟异步等待
    std::cout << "After co_await\n";
}

int main() {
    simple();
    std::cout << "Program end\n";
    return 0;
}

该程序会先输出 “Before co_await”,随后暂停,最后在 main 结束前输出 “After co_await”。

2. 协程实现原理

2.1 状态机生成

编译器把协程函数拆分为若干“步”,每个 co_await/co_yield/co_return 处都会产生一条分支。所有分支通过一个 switch 或状态机表驱动,保证程序在暂停后能够恢复到正确的位置。

2.2 Promise 对象

Promise 对象负责:

  • 保存协程局部变量(如 this、参数等)
  • 提供 get_return_objectinitial_suspendfinal_suspendreturn_void/return_valueunhandled_exception 等接口
  • 存放协程的“悬挂”或“完成”状态

2.3 资源管理

协程生成器对象与其 promise_type 必须在同一生命周期内,否则会出现悬挂的 std::coroutine_handle

  • co_return 时,final_suspend 的返回值决定协程是否自动销毁。
  • 若返回 std::suspend_never,协程会立即完成并销毁。

3. 常见协程使用场景

3.1 异步 I/O

std::async 或网络库中,协程可替代回调链,让异步 I/O 看似同步。

awaitable<std::string> fetch_from_server(const std::string& url) {
    auto socket = co_await tcp_connect(url);
    std::string resp = co_await socket.read_some();
    co_return resp;
}

3.2 生成器

使用 co_yield 可实现懒加载序列,例如斐波那契数列、文件行迭代器等。

generator <int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int tmp = a + b;
        a = b;
        b = tmp;
    }
}

3.3 任务调度

在游戏引擎或 UI 框架中,协程可做帧间调度,让长时间任务分步执行。

4. 常见坑与最佳实践

序号 坑点 解决方案
1 协程对象被移动后访问 coroutine_handle 确保协程对象在移动前后保持有效,避免裸指针
2 co_await 的异步对象没有 await_ready/await_suspend 自定义 awaitable 时实现完整协议
3 内存泄漏:promise 未被销毁 final_suspend 必须返回 std::suspend_never 或手动销毁 handle
4 性能开销 使用 std::suspend_always / std::suspend_never 选择性暂停,避免不必要的上下文切换
5 与旧版库混用 尽量使用统一的异步框架(如 Boost.Asio),避免不同协程实现混合

4.1 自定义 awaitable 示例

struct Timer {
    std::chrono::milliseconds duration;
    Timer(std::chrono::milliseconds d) : duration(d) {}
    bool await_ready() const noexcept { return duration.count() == 0; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, this](){
            std::this_thread::sleep_for(duration);
            h.resume();
        }).detach();
    }
    void await_resume() const noexcept {}
};

调用方式:

co_await Timer( std::chrono::seconds(1) );

5. 小结

C++20 的协程为语言带来了更接近自然流程的异步编程模型。通过了解协程的语法、状态机实现以及 Promise 的角色,程序员可以在保持代码可读性的同时,高效地实现异步 I/O、生成器、任务调度等功能。与此同时,需要注意协程对象的生命周期、awaitable 的完整协议以及性能权衡,避免常见陷阱。随着未来标准的进一步完善(C++23 将推出 std::generator 等),协程将在更广泛的场景中发挥作用。

C++20 模块(Modules)在大型项目中的应用与实践

在 C++20 之前,头文件的包含机制虽然广为人知,但却存在编译速度慢、二义性冲突、依赖图难以可视化等问题。C++20 引入了模块(Modules)这一新特性,旨在解决这些痛点。本文将从模块的基本概念入手,阐述它在大型项目中的实际应用,并分享一些经验与最佳实践。

1. 模块概念回顾

  • 模块定义:模块由一个或多个模块单元(module unit)组成,每个单元是一个包含导出声明的源文件。模块单元使用 export 关键字声明可被其他模块导入的内容。
  • 导入语法:使用 import 模块名; 语句导入模块。编译器会生成模块接口文件(.ifc),在导入时直接使用,而不需要再次编译源文件。
  • 模块接口:模块接口文件描述了模块公开的符号。它是编译器生成的二进制文件,类似于传统头文件,但更高效。

2. 模块带来的优势

传统头文件 模块
包含时需重新编译 只编译一次,导入时直接使用二进制
编译速度慢 编译速度显著提升
容易产生命名冲突 通过模块作用域隔离,减少冲突
依赖关系难以可视化 模块依赖树可通过工具生成
需要手动管理头文件 编译器自动管理依赖关系

3. 在大型项目中的应用步骤

3.1 评估现有代码

  • 识别重复包含:使用工具(如 clang-Xclang -ast-dump)检查哪些头文件被多次包含。
  • 确定模块化粒度:按功能划分模块,例如 Graphics, Physics, Audio 等。每个模块内部可以进一步拆分为子模块。

3.2 迁移到模块

  1. 创建模块单元
    // graphics.ixx
    export module graphics;
    export class Renderer { /*...*/ };
    export void init(); // ...
  2. 更新构建系统
    • 对于 CMake:使用 target_sources 指定 .ixx 文件,target_link_libraries 通过模块名链接。
    • 对于 Bazel:使用 cc_library 并添加 modules 属性。
  3. 调整 #include
    • #include "renderer.h" 替换为 import graphics;
    • 对于旧头文件,需要将其迁移为模块接口,或保留为辅助头文件但不导出。

3.3 处理第三方库

  • 已有模块化:若第三方库已提供模块化接口,直接使用 import
  • 自定义包装:为非模块化库编写包装模块,将其公开接口包裹在 export 声明中。

3.4 性能与构建优化

  • 预编译模块:在 CI 里缓存 .ifc 文件,避免每次构建都重新生成。
  • 并行编译:将模块单元划分为不相互依赖的块,以提升并行度。
  • 增量构建:利用编译器的增量构建特性,仅重新编译被修改的模块单元。

4. 常见陷阱与解决方案

陷阱 解决方案
模块与头文件混用导致编译错误 保证同一功能只在模块或头文件中出现,避免重复定义。
依赖循环 通过 export 只暴露必要符号,拆分模块或使用前向声明。
编译器兼容性 确认使用的编译器(如 GCC 11+, Clang 13+, MSVC 19.28+)已完全支持模块。
代码风格不一致 在迁移过程中统一命名约定与访问修饰符。

5. 案例分享:某游戏引擎的模块化改造

项目背景:原有引擎使用大量头文件,编译时间达 30 秒。
改造过程:

  1. 分层拆分:把渲染、物理、音频、脚本等功能拆分为独立模块。
  2. 模块化接口:为每层生成 .ixx 文件,暴露核心类与函数。
  3. CI 流水线:在 Jenkins 上缓存模块接口,保证增量构建。
  4. 结果:编译时间从 30 秒缩短至 12 秒,构建成功率提升 15%。
  5. 维护成本:由于模块化,依赖关系一目了然,代码复用率提升。

6. 结语

C++20 模块为大型项目提供了更高效、可维护的构建方式。虽然迁移过程需要一定投入,但长期来看,它能显著提升编译速度、降低命名冲突风险,并让依赖管理更为透明。建议团队在评估现有代码时先做小范围实验,逐步将关键模块迁移为 C++ 模块,最终实现完整的模块化体系。祝你在 C++ 模块的道路上一帆风顺!

**如何在C++中实现自定义智能指针的拷贝与移动语义?**

在现代 C++ 开发中,智能指针(std::unique_ptrstd::shared_ptr)已经成为了管理动态内存的标准工具。然而,某些特殊场景下我们可能需要自己实现一个自定义的智能指针,例如实现一个对某种资源(文件句柄、网络连接等)具有更细粒度控制的指针。下面我们从设计思路、关键成员函数实现以及拷贝/移动语义的细节来完整阐述如何实现一个简单但功能完整的自定义智能指针。


1. 设计目标与基本框架

1.1 目标

  • 资源管理:负责释放持有的资源,防止泄漏。
  • RAII:资源在对象生命周期内保持唯一拥有权。
  • 拷贝:不可拷贝(类似 unique_ptr),但可以实现自定义拷贝构造来支持浅拷贝或引用计数。
  • 移动:支持移动构造和移动赋值,以便对象可以在容器中移动而不拷贝资源。
  • 线程安全:仅在需要时才引入原子计数器。

1.2 基本类定义

template<typename T, typename Deleter = std::default_delete<T>>
class CustomUniquePtr {
public:
    // 构造
    explicit CustomUniquePtr(T* ptr = nullptr) noexcept;
    explicit CustomUniquePtr(std::unique_ptr<T, Deleter>&& other) noexcept; // 移动构造

    // 析构
    ~CustomUniquePtr();

    // 禁止拷贝
    CustomUniquePtr(const CustomUniquePtr&) = delete;
    CustomUniquePtr& operator=(const CustomUniquePtr&) = delete;

    // 移动
    CustomUniquePtr(CustomUniquePtr&& other) noexcept;
    CustomUniquePtr& operator=(CustomUniquePtr&& other) noexcept;

    // 访问
    T* get() const noexcept;
    T& operator*() const noexcept;
    T* operator->() const noexcept;

    // 释放资源
    void reset(T* ptr = nullptr) noexcept;
    T* release() noexcept;

private:
    T* ptr_;
    Deleter deleter_;
};

2. 成员函数实现

2.1 构造与析构

template<typename T, typename Deleter>
CustomUniquePtr<T, Deleter>::CustomUniquePtr(T* ptr) noexcept
    : ptr_(ptr), deleter_() {}

template<typename T, typename Deleter>
CustomUniquePtr<T, Deleter>::~CustomUniquePtr() {
    if (ptr_) deleter_(ptr_);
}

2.2 移动构造与移动赋值

template<typename T, typename Deleter>
CustomUniquePtr<T, Deleter>::CustomUniquePtr(CustomUniquePtr&& other) noexcept
    : ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
    other.ptr_ = nullptr;
}

template<typename T, typename Deleter>
CustomUniquePtr<T, Deleter>&
CustomUniquePtr<T, Deleter>::operator=(CustomUniquePtr&& other) noexcept {
    if (this != &other) {
        reset();
        ptr_ = other.ptr_;
        deleter_ = std::move(other.deleter_);
        other.ptr_ = nullptr;
    }
    return *this;
}

2.3 资源管理函数

template<typename T, typename Deleter>
void CustomUniquePtr<T, Deleter>::reset(T* ptr) noexcept {
    if (ptr_ != ptr) {
        if (ptr_) deleter_(ptr_);
        ptr_ = ptr;
    }
}

template<typename T, typename Deleter>
T* CustomUniquePtr<T, Deleter>::release() noexcept {
    T* tmp = ptr_;
    ptr_ = nullptr;
    return tmp;
}

2.4 访问操作符

template<typename T, typename Deleter>
T* CustomUniquePtr<T, Deleter>::get() const noexcept { return ptr_; }

template<typename T, typename Deleter>
T& CustomUniquePtr<T, Deleter>::operator*() const noexcept { return *ptr_; }

template<typename T, typename Deleter>
T* CustomUniquePtr<T, Deleter>::operator->() const noexcept { return ptr_; }

3. 支持引用计数(可选)

如果想让 CustomUniquePtr 在拷贝时实现共享(类似 std::shared_ptr),可以把内部指针改为指向结构体:

struct ControlBlock {
    T* ptr;
    std::atomic <size_t> ref_count;
    Deleter deleter;
    ControlBlock(T* p, Deleter d)
        : ptr(p), ref_count(1), deleter(d) {}
};

template<typename T, typename Deleter>
class CustomSharedPtr {
public:
    explicit CustomSharedPtr(T* ptr = nullptr) : ctrl_(nullptr) {
        if (ptr) ctrl_ = new ControlBlock(ptr, Deleter{});
    }
    // 拷贝构造
    CustomSharedPtr(const CustomSharedPtr& other) noexcept
        : ctrl_(other.ctrl_) {
        if (ctrl_) ++ctrl_->ref_count;
    }
    // ...
private:
    ControlBlock* ctrl_;
};

此时要实现析构时递减计数,并在计数为 0 时释放资源。


4. 使用示例

int main() {
    CustomUniquePtr <int> p(new int(42));
    std::cout << *p << '\n'; // 输出 42

    // 移动
    CustomUniquePtr <int> q(std::move(p));
    if (!p.get()) std::cout << "p 为空\n";

    // reset
    q.reset(new int(100));
    std::cout << *q << '\n';

    // release
    int* raw = q.release();
    std::cout << *raw << '\n';
    delete raw; // 手动释放

    return 0;
}

5. 小结

  • RAII 原则:自定义智能指针通过构造/析构管理资源,天然满足 RAII。
  • 移动语义:实现移动构造和移动赋值,让对象在容器中移动成本低。
  • 不可拷贝:默认不可拷贝,保持资源唯一性;如需共享,可在内部引入引用计数。
  • 可扩展性:可以在 Deleter 模板参数中传入自定义删除器,例如文件句柄、网络套接字等。

通过上述实现,你可以快速构建符合自己需求的智能指针,并在 C++ 项目中享受 RAII 与现代语义带来的安全性与便利。

C++20概念(Concepts)实战案例:让模板更安全、更易读

概念(Concepts)是C++20引入的一项强大特性,它允许我们在模板参数中声明约束,从而在编译阶段就能对类型进行更细粒度的检查。相比传统的SFINAE技巧,概念让代码更加直观、易维护。下面我们通过一个完整的实战案例,展示如何使用概念来实现一个通用的“可排序容器”框架,并结合现代C++最佳实践进行优化。

1. 需求描述

我们想要实现一个函数模板 sort_container,能够对任何满足排序需求的容器进行升序排序。传统的实现往往需要:

template<typename Container>
void sort_container(Container& c) {
    static_assert(std::is_same_v<decltype(std::begin(c)), typename Container::iterator>, "Container must be iterable");
    std::sort(std::begin(c), std::end(c));
}

但这段代码缺乏可读性,且错误信息不够直观。利用概念,我们可以写出更明确、可读性更高的版本。

2. 定义基本概念

2.1 可迭代容器(Iterable)

#include <concepts>
#include <iterator>

template<typename T>
concept Iterable = requires(T t) {
    std::begin(t);
    std::end(t);
};

2.2 可排序容器(Sortable)

我们进一步要求容器的迭代器满足可比较,并且可以被 std::sort 处理。为此,我们声明:

template<typename T>
concept Sortable = Iterable <T> &&
    std::sortable<decltype(std::begin(std::declval<T&>()))>;

std::sortable 是 C++20 提供的概念,用来检查迭代器是否满足 std::sort 的需求(可随机访问、可比较等)。

3. 实现 sort_container

#include <algorithm>

template<Sortable C>
void sort_container(C& container) {
    std::sort(std::begin(container), std::end(container));
}

此函数现在会在编译阶段直接检查 container 是否满足 Sortable,如果不满足,编译器会给出清晰的错误信息,例如:

error: no matching function for call to 'sort_container'
note: candidate template ignored: constraint 'Sortable <T>' not satisfied

4. 应用示例

#include <vector>
#include <list>
#include <set>
#include <iostream>

int main() {
    std::vector <int> vec = {5, 3, 4, 1, 2};
    sort_container(vec);
    for (int x : vec) std::cout << x << ' ';
    std::cout << '\n';

    std::list <double> lst = {2.5, 1.2, 3.8};
    sort_container(lst); // OK,list 支持随机访问迭代器

    // std::set <int> s = {3, 1, 2};
    // sort_container(s); // 编译错误:std::set 的迭代器不可随机访问
}

注意:std::set 的迭代器不是随机访问的,因此不满足 Sortable。如果需要对 std::set 进行排序,通常的做法是将其复制到 std::vector 或者使用 std::inplace_merge 等算法。

5. 与传统 SFINAE 对比

传统实现可能采用以下技巧:

template<typename T, typename = std::enable_if_t<...>>
void sort_container(T& c);

这种写法容易导致错误信息难以定位,并且需要手动指定各种约束。概念则把约束写在模板参数列表中,语义更清晰,也使得错误信息更具可读性。

6. 小结

  • 概念让模板参数的约束声明更直观、语义化。
  • std::sortable 提供了对 std::sort 的内置约束,减少自定义约束的工作量。
  • 通过组合基本概念(如 Iterable)可以快速构建更复杂的约束。
  • 在实际项目中,建议对常用的模板功能使用概念来替代传统的 SFINAE 或 static_assert,从而提升代码可维护性和开发效率。

下一个挑战:尝试用概念实现一个“可映射容器”(支持 operator[])的通用函数,进一步巩固概念的使用。

**C++ 23:新标准中的 constexpr 与即时编译**

自 C++11 起,constexpr 已经被加入语言,用来在编译期计算常量表达式。随着 C++20 与 C++23 的发展,constexpr 的功能和限制已经发生了显著扩展。本文将重点介绍 C++23 中 constexpr 的新特性、实现细节以及在实际项目中的应用场景。

1. 关键特性回顾

版本 关键改动
C++11 引入 constexpr,仅限于常量表达式的函数与变量
C++14 允许循环与递归,局部静态变量
C++17 if constexprswitch constexpr
C++20 constexpr 模块化、std::bit_caststd::is_constant_evaluated
C++23 constexpr 的完全常量表达式支持、允许动态分配、支持 std::string_view 构造

C++23 将 constexpr 的能力推向极致,使得几乎所有合法的表达式都可以在编译期评估,只要满足 constexpr 语义所需的条件。

2. constexpr 的实现原理

在编译器内部,constexpr 语句会被视为两条代码路径:编译期执行运行时执行。编译器通过 constexpr 评估器(Constant Evaluator,CE)执行表达式,若所有操作符与函数均满足 constexpr 规则,CE 将把结果嵌入到目标代码中;否则会退回到运行时执行。

关键点如下:

  1. 内存访问
    C++23 允许 newdeleteconstexpr 上下文中使用。CE 需要在一个可预测的编译期内存管理器中分配和释放对象,并记录其生命周期。

  2. 异常处理
    constexpr 允许抛异常,但在编译期如果抛异常则被视为不满足 constexpr 规则。CE 在评估过程中会捕获异常并将其视为错误。

  3. 外部依赖
    constexpr 只能依赖 已知在编译期可用 的符号。C++23 中支持 extern constexpr,但仍需满足链接时的常量表达式约束。

3. 实际案例

下面通过几个代码片段演示 C++23 中 constexpr 的强大功能。

3.1 递归求斐波那契数列

constexpr std::size_t fib(std::size_t n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
static_assert(fib(10) == 55);

在 C++23 中,递归深度可达数千,CE 能够处理更大的编译期计算。

3.2 constexpr 动态数组

constexpr std::array<int, 5> initArray() {
    std::array<int, 5> a{};
    for (std::size_t i = 0; i < a.size(); ++i) {
        a[i] = static_cast <int>(i * i);
    }
    return a;
}
static_assert(initArray()[2] == 4);

C++23 允许在 constexpr 中使用 new 创建临时对象,甚至支持 std::string 的构造。

3.3 constexpr 与模块

module my_math;

export constexpr double pi = 3.1415926535897932385;

模块化 constexpr 变量可以在编译期跨模块共享,提高链接速度与模块化安全性。

4. 性能与限制

  • 编译时间:大量 constexpr 计算会显著增加编译时间,尤其是递归或大数组的初始化。合理使用 static_assert 与分块计算可缓解此问题。
  • 内存占用:CE 需要维护编译期内存模型,导致大型编译单元的内存使用上升。可以通过 -fconstexpr-steps 选项调节评估步骤数。
  • 移植性:并非所有编译器都已完整实现 C++23 的 constexpr 扩展。务必在目标平台上测试。

5. 结语

C++23 将 constexpr 的范畴扩展到几乎所有可在编译期评估的表达式,使得我们能够在编译期完成更多计算,提升运行时性能并减少错误。熟练运用 constexpr 与新特性,可让代码更加安全、可读且高效。未来的 C++ 版本将进一步加强编译期计算与执行的桥接,预计会出现更多关于“编译时执行”与“运行时执行”混合的创新用法。

C++20 中的模块(Modules)如何提高编译效率?

在传统的头文件包含模型中,编译器需要一次又一次地解析相同的头文件,导致大量重复工作。C++20 引入的模块系统通过将编译单元拆分为独立的、可预编译的模块来解决这一问题。

1. 模块的基本概念

  • 模块导出(module export):在模块接口文件(.ixx)中使用 export module 模块名; 声明模块。所有在此文件中使用 export 关键字导出的符号会成为该模块的公共接口。
  • 模块使用(module use):在需要引用模块的文件中使用 import 模块名;,编译器将直接读取已编译好的模块预编译单元(.pcm),而不必重新解析源代码。

2. 编译效率提升原理

  • 消除重复包含:传统头文件包含会导致同一文件多次解析。模块只解析一次,随后所有引用直接加载预编译单元。
  • 更好地隔离编译单元:模块边界更清晰,编译器可以在更大范围内并行编译不同模块,减少依赖链的长度。
  • 预编译单元的共享:在多项目共享同一模块时,只需编译一次,其他项目直接引用。

3. 实践步骤

步骤 说明 代码示例
1 创建模块接口文件(lib.ixx) export module lib; export int add(int a, int b);
2 实现模块实现文件(lib.cpp) module lib; int add(int a, int b){return a+b;}
3 编译模块 g++ -std=c++20 -fmodules-ts -c lib.cpp -o lib.o(或使用支持的编译器参数)
4 在使用方编译 g++ -std=c++20 -fmodules-ts -o main main.cpp lib.o
5 在使用方文件中导入模块 import lib; int main(){return add(2,3);}

4. 典型问题与解决

  • 编译器兼容性:目前 GCC、Clang、MSVC 对模块的支持仍在完善,确保使用最新版。
  • 头文件混用:在同一文件中混用 #includeimport 时,要确保 #include 的头文件不再被模块导入,避免重复。
  • 链接时的模块导出:在大型项目中,建议使用 -fmodules-cache-path 指定缓存路径,避免重复编译。

5. 未来展望
随着标准化和编译器成熟,模块系统将进一步优化编译时间、提升大型项目的构建速度,并推动更安全、可维护的 C++ 代码库。关注 C++20 之后的标准迭代,将模块作为核心特性继续发展。

使用 C++20 的概念(Concepts)实现安全的泛型编程

在 C++20 之前,泛型编程主要靠模板和 SFINAE(Substitution Failure Is Not An Error)来约束类型。虽然可行,但错误信息往往晦涩难懂,导致调试成本高。C++20 引入了 概念(Concepts),提供了一种更直观、可读性更好的方式来限定模板参数的要求。

1. 什么是概念?

概念是一种类型约束,类似于接口,但只在编译期检查。它描述了一个类型或表达式必须满足的属性或操作,例如可迭代、可比较、数值类型等。概念可以直接用于模板参数列表,让编译器在不满足约束时给出清晰的错误提示。

#include <concepts>
#include <iostream>

template <typename T>
concept Incrementable = requires(T x) {
    { ++x } -> std::same_as<T&>;
    { x++ } -> std::same_as <T>;
};

template <Incrementable T>
void increment_all(T& container) {
    for (auto& elem : container) {
        ++elem;
    }
}

2. 如何定义概念?

概念可以使用 requires 语句或 requires 表达式来定义。requires 语句更强大,支持对多个参数进行约束,并允许在其中包含 if constexpr 等逻辑。

template <typename T>
concept HasSize = requires(T a) {
    { a.size() } -> std::convertible_to<std::size_t>;
};

template <HasSize T>
void print_size(const T& obj) {
    std::cout << "Size: " << obj.size() << '\n';
}

3. 组合概念

C++20 允许使用逻辑运算符组合概念,形成更细粒度的约束。例如,定义一个 “可比较且可散列” 的概念:

template <typename T>
concept ComparableAndHashable = std::totally_ordered <T> && std::hashable<T>;

template <ComparableAndHashable T>
void process(const T& key) {
    // ...
}

4. 概念在 STL 容器中的应用

标准库中的许多容器已使用概念来约束算法。例如,std::ranges::sort 要求可随机访问迭代器和可比较元素。通过概念,编译器能够在调用时立即检查不满足的类型,并给出准确的错误信息。

#include <algorithm>
#include <vector>
#include <ranges>

std::vector <int> v{3, 1, 4, 1, 5};
std::ranges::sort(v); // 正确

如果传入不满足条件的类型,编译器会提示:

error: no matching function for call to ‘sort’

而不是更模糊的 SFINAE 消息。

5. 迁移到概念的策略

  • 识别最常用的约束:从项目中最频繁使用的模板开始,给它们添加概念。
  • 编写单元测试:确保新概念不会导致现有代码失效。
  • 逐步替换:先在新代码中使用概念,后期再对旧代码进行改造。
  • 文档化:在函数注释中说明使用的概念,让使用者一目了然。

6. 常见误区

  1. 概念不是运行时检查:它们只在编译期生效,不能替代异常处理或断言。
  2. 不要过度约束:概念的目的是提高可读性和错误信息,过度约束可能导致过多的重载冲突。
  3. 不要忽视标准库:先尝试使用已有的标准概念(如 std::same_as, std::derived_from),避免重新实现。

7. 结语

概念是 C++20 对泛型编程的重大改进,显著提升了代码的可维护性和错误定位效率。通过合理定义和使用概念,开发者可以写出更安全、更易读的模板代码,并让编译器为我们自动完成大量类型检查的工作。未来,随着标准库持续扩展概念的使用,C++的泛型编程生态将变得更加成熟与强大。