C++模块化编程:告别#include的新时代

我看网上的一些文档,里面有很多人讲import,对此来讲一下个人见解。
事先声明:本文仅作技术讨论与交流,部分术语可能为非标准表述(如“接口污染”等概念),旨在通俗化描述技术问题。文中观点或示例可能存在不严谨之处,欢迎专业人士指正。

第一章 差异变化

从C++98到C++17,C++一直沿用基于 #include的“库导入”方式来组织代码。这种方式本质上是文本替换:预编译器将头文件的内容原封不动地复制到源文件中,形成一个巨大的编译单元。如:

//C++98及以后的标准 #include<iostream>//导入库 int main()//程序入口 { std::cout<<"Hello,World!"; //隐式加入return 0; }

这里是将代码变为:

// iostream standard header // Copyright (c) Microsoft Corporation. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #ifndef _IOSTREAM_ #define _IOSTREAM_ #include <yvals_core.h> #if _STL_COMPILER_PREPROCESSOR #include <istream> #include <ostream> //等等,以上仅作演示,可能不对 int main() { std::cout<<"Hello,World!"; }

这种机制带来了几个显著问题:

  1. 编译速度慢:每个编译单元(.cpp文件)都要独立、重复地解析这些庞大的头文件内容,工程量指数级增长。
  2. 封装性被破坏:头文件中的所有内容(包括实现细节、私有宏)都会暴露给包含它的源文件,没有真正的信息隐藏。
  3. 依赖管理复杂:头文件的层层包含容易导致循环依赖、宏污染等问题。

不过有了import后,代码可以变为这样

import std; int main() { std::print("Hello,World"); //隐式添加return 0; }
注:这里有人会问:“这不是用了C++23的内容吗?”确实,std::print是 C++23 的新特性。但更重要的是,import std;这种导入整个标准库作为模块的方式,也是 C++23 才正式标准化的。C++20 虽然引入了模块语法,但并未为标准库提供模块化支持。具体可参考微软文档 教程:使用命令行中的模块导入 C++ 标准库。

这里本质是寻找std::print这个接口,并调用实现,能极大限度的保证封装性

同时在编译的角度里,他仅仅只是调用接口使用,所以编译时间完虐#include

依赖管理这个更明显了,非常清晰的表示“我需要std这个模块里的接口”。

所以,模块化可能是C++工程中最革命的一环

第二章 怎么实现模块

在传统C++中,我们用 .hpp声明接口,用 .cpp实现功能。但这只是逻辑上的分离——只要 #include了头文件,里面的 private成员、内部类型,全都暴露无遗。这就像你家装修,虽然卧室门上挂了“私有”的牌子,但墙却是玻璃的,路过的谁都能瞅一眼。

模块的出现,就是为了砌上这堵实心的墙

至于怎么实现……见下

2.1 基于现有的代码进行实现

传统C++工程通常采用.hpp声明与.cpp实现分离的方式,前者定义接口规范,后者完成具体实现。

模板化改造后演变为.ixx接口文件与.cpp实现文件的组合。虽然形式上相似,但设计思维存在本质差异:.ixx通过抽象接口描述组件契约,.cpp则负责接口的具体外部实现,形成更松散的模块化耦合。

以下为例子

转换前:

// CharacterBase.hpp #pragma once #include<string> namespace GameSet { class NameID { /* ... */ }; class Performance { /* ... */ }; class MoveSpeed { /* ... */ }; };
// CharacterBase.cpp #include"CharacterBase.hpp" namespace GameSet { // ... 实现代码 };

转换后:

// CharacterBase.ixx export module CharacterBase; // 确定模块名,为后续导入 // import std; // 导入std,但不知为何这个会出错,故先用#include #include <string> export namespace GameSet // export声明这个命名空间是公有的 { class NameID { /* ... */ }; class Performance { /* ... */ }; class MoveSpeed { /* ... */ }; };
// CharacterBase.cpp module CharacterBase; // 声明这个.cpp是CharacterBase模块里的 namespace GameSet { // ... 实现代码(与转换前几乎相同) };

几个关键变化:

  1. #pragma once → export module 模块名;
    从“防止本文件被重复包含”的防御性指令,升级为“主动声明一个模块身份”的构建性指令。
  2. #include <string> → import std;
    从“复制粘贴文本”变为“导入预编译的接口”。这是编译速度产生质变的核心。
  3. 在需要公开的代码前添加 export
    这是模块的“守门人”。只有被 export 导出的符号(类、函数、命名空间)才会成为模块的公有接口。未导出的部分(包括整个类内部的私有成员)对外部代码完全隐形,实现了真正的物理封装。
  4. 实现文件首行写 module 模块名;
    这行代码告诉编译器:“本文件是某模块的实现部分,我拥有访问其全部内部细节的权限。”因此,实现代码可以无缝使用模块接口中定义的私有成员。

(顺便说一句,我的代码写得有点shit)

不过这仅仅只是有的情况下,万一团队从头开始呢?

2.2 从头开始编写.ixx文件

如果你正在关注C++26版本,建议在看完本教程后查阅std模块是否已被封装。本教程基于C++23标准编写。

C++提供了丰富的声明方式,例如变量声明支持超过20种不同的语法形式,模块导入机制也包含12种具体方法,详细内容可参考微软文档中关于module、import和export的说明。

这边展示最基础的

//mymodule.ixx export module mymodule;//声明模块名字 /*namespace mymodule*///可选,但建议加上,原因见后面 int fier();//私有接口 void slow();//私有接口 export void set();//公有接口
//mymodule.cpp module mymodule;//声明接口 set() { //dosomething }

当其他文件import mymodule;时:

  • 能看到api()函数
  • 完全看不到helper()internal()函数

为什么建议加上命名空间?

虽然模块名本身提供了一层隔离,但在模块内部使用命名空间(如namespace mymodule { ... })仍然是好习惯。这能避免模块内符号污染全局空间,并与现有大量使用命名空间的代码风格保持一致。

第三章 当前import机制存在的痛点

本章为个人观点

尽管模块功能强大,但仍存在几个明显的缺陷需要注意

3.1 官方库的缺点

3.1.1 编译器并未完全普及

如标题所示,当前编译器对C++17的支持最为完善。虽然C++20和C++23已在实验性支持(MSVC)或部分支持(Clang)阶段,但尚未达到普及程度。预计在下一版本C++中,这些新特性将获得全面支持。

注意:2.1代码中 import std;在某些编译器或项目配置下可能无法通过编译。这是因为标准库模块 (std) 是 C++23 才正式引入的特性,且需要在编译器中明确开启支持(如在 MSVC 中需使用 /std:c++23并配合特定编译开关)。如果你遇到错误,一种临时的兼容方式是回退到在 .ixx文件中使用 #include <string>,但这会牺牲部分模块化优势。这正说明了新特性的普及仍需时日。

3.1.2 导入功能过多

当前std模块同时包含IO交互和STL库功能,甚至包含一些使用频率较低的特性,这违背了DRY和YAGNI原则。因此在2.2节特别说明:"如果你关注C++26版本,建议学完本教程后查阅std模块的最新封装情况。本教程内容基于C++23标准编写。"

3.2 接口/命名空间污染

随着接口数量持续增长,std模块功能过于庞杂的问题日益突出,容易引发同名接口冲突。为此,必须对std模块进行重构和细化封装。同时,编译器需要提供相应警告机制(或通过制定编码规范)来预防此类问题的发生。

为避免全局命名空间污染的风险,谨慎设计模块的接口边界至关重要。

3.3 工程中会同时出现#include<>与import

混合使用难免降低可读性,仅用单一方式又容易产生问题,这确实是个两难困境。这是一个典型的“过渡期阵痛”

第四章 工程中比较推荐的写法。

注:本部分为个人推荐,作者并未发现任何文档能支持

4.1 传统导入库+现代导入模块

将常用代码模块化虽然会增加一些复杂性,能显著提升整体代码的可读性。如:

export module CharacterBase; import std; export namespace GameSet { class NameID { private: std::string m_name; std::string m_id; public: NameID(std::string const&, std::string const&); std::string GetName()const; std::string GetID()const; void SetName(std::string const&); };//存储玩家社交信息。 class Performance { private: float m_current_health; float m_max_health; public: Performance(float); float GetMaxHealth()const; float GetCurrentHealth()const; void SetCurrentHealth(float); void SetMaxHealth(float); };//之后基础属性往这里塞 class MoveSpeed { float m_speed; public: MoveSpeed(float); float GetSpeed()const; void SetSpeed(float); };//之后进阶属性往这里塞 };

std::string封装为一个专门的模块

// StringModule.ixx export module StringModule; #include <string> export class StringWrapper { public: std::string data; // 这里仅仅只是想用string而已,未必需要复杂封装。 };

重新导入(记得修改一下)

// CharacterBase.ixx (修改后) export module CharacterBase; import StringModule; // 导入我们自制的模块 export namespace GameSet { class NameID { private: StringWrapper m_name; // 使用自制的字符串包装类 StringWrapper m_id; public: // ... 接口声明 }; // ... 其他类 };

这样做看似多此一举,但能精确控制依赖,减少不必要的接口暴露。

4.2 自创模块+实现导入库

这个思路就非常简单:将所有公开接口抽象到.ixx文件中,所有具体实现留在.cpp文件里。这正是本文2.1节所展示的模式,也是模块化最核心、最推荐的做法。

4.3 避免反复导入:保持依赖清晰

为什么不推荐在模块中反复使用#include

正如在3.3节提到的,在工程中混合使用#include<>import是常见的过渡期现象,但这会带来可读性和维护性的问题。具体到模块内部,反复导入库的弊端更加明显:

  1. 破坏封装一致性:一个模块的理想状态是对外提供明确的接口,对内隐藏所有实现细节。如果在模块接口文件(.ixx)中大量使用#include,相当于又把外部库的实现细节“泄露”到了模块的接口边界上,削弱了模块的封装价值。
  2. 编译优势被抵消:模块的核心优势之一是避免重复编译。但如果你在每个模块里都#include <vector>,那么vector的代码又会在每个模块的编译过程中被重复处理,部分抵消了模块带来的编译加速收益。
  3. 依赖关系复杂化:假设模块A和模块B都#include <some_library.h>,而some_library.h发生了改变,那么A和B都需要重新编译。如果它们通过一个共同的、稳定的模块接口来间接使用该功能,则可能只需要重新编译那个接口模块。

工程建议:

  • 优先import:对于已模块化的标准库组件(C++23的std/std.core),坚持使用import。(虽然4.1代码块并没有这么做,更多是收敛)
  • 收敛#include:对于尚未模块化的第三方库或平台头文件,尽量将其集中到少数几个“适配模块”中。其他业务模块只import这些适配模块,而不是直接#include原始头文件。
  • 保持接口纯洁:模块的.ixx文件应尽可能只包含import语句和export接口声明,将具体的实现细节(包括必要的#include)推到.cpp实现文件中。

简单的说:把模块当成一个“边界清晰的盒子”。盒子里怎么折腾(.cpp里#include)是内部事务,但盒子对外展示的接口(.ixx)应该尽可能干净、稳定、不暴露内部所用的具体工具。

第五章 对未来的猜测

注:本章为个人观点,无任何文档证实。

5.1 Unity的实验性兼容

考虑到Unity引擎底层大量使用C++,且对性能、编译速度和跨平台有极高要求,模块几乎是Unity未来无法回避的技术选项

我猜测Unity团队很可能已经在内部进行模块的评估和实验,原因有三:

  1. 编译加速需求:Unity项目的编译时间一直是开发者痛点。模块带来的编译期改进,对动辄数小时的完整项目构建是巨大的诱惑。
  2. 更好的封装:Unity的代码库极其庞大,模块提供的物理级别封装,能更好地隔离引擎核心代码与用户脚本、第三方插件,减少意外耦合。
  3. C++生态演进:作为主流游戏引擎,Unity需要跟上C++标准的步伐,确保长期的技术竞争力。

不过,这种兼容很可能是渐进式的:先在内部工具链、部分子系统试用,再逐步向用户开放的插件系统或脚本后端渗透。我们可能会在未来的Unity版本中看到类似#pragma enable_cpp_modules的编译选项,或是对import语句的实验性支持。

5.2 std模板拆分

当前import std;这种“大一统”的导入方式,几乎肯定会在未来版本中被拆分。原因正是我们在3.1.1节(导入功能过多)和3.2节(接口污染)​ 所分析的:

  1. 违背“按需索取”原则:现代C++强调“零开销抽象”,而强迫用户导入整个标准库来使用一个std::vector,本身就是一种抽象开销。
  2. 工程可维护性差:巨型模块会拖慢IDE的智能提示、增加符号解析负担,并让依赖关系变得模糊。
  3. 生态系统压力:第三方库作者会需要更细粒度的依赖控制,而不是绑定整个std

我猜想到C++26或C++29,我们会看到更精细的模块划分,比如:

// 未来的理想导入方式(猜测) import std.core; // 最基础的运行时支持 import std.containers; // vector, map, set等 import std.threading; // thread, mutex, atomic import std.io; // iostream, format // 而不是现在的 import std; (一股脑全进来)

这种拆分不仅能解决接口污染问题,还能让编译器的增量编译、并行编译更加高效。当然,为了向后兼容,import std;这个“快捷方式”很可能还会保留,但它会逐渐被编码规范标记为“不推荐”。

第六章 总结:模块,C++工程的静默革命

注:本部分由ai生成

回顾这场从#includeimport的迁移,我们看到的不仅是一次语法更新,更是C++工程哲学的一次深刻转变。

模块带来的核心价值可以用三个词概括:

  1. 真正的封装——不再是“约定俗成”的private,而是编译器强制的物理边界
  2. 编译革命——从每个文件重复解析的“体力劳动”,到一次编译多次复用的“智能缓存”
  3. 依赖清晰——从隐晦的#include链到显式的import声明

但正如我们所讨论的,这场革命仍在进行中。C++23的std模块更像一个“概念验证”,编译器支持参差不齐,工程实践也还在摸索。我们既要拥抱模块带来的新可能,也要清醒地认识到它的过渡期特性——混合使用#includeimport、谨慎设计模块边界、关注编译器支持度,这些都是现阶段必须考虑的务实问题。

模块不会一夜之间取代所有#include,就像nullptr没有立即淘汰NULL一样。C++的演进总是渐进的、兼容的。但趋势已经明确:模块代表了C++对大规模工程、快速编译、清晰架构的追求。

作为开发者,我们现在要做的不是立即重写所有代码,而是开始理解模块的思想,在合适的场景尝试,并关注标准的演进。当C++26带来更精细的std.vectorstd.thread模块时,当更多编译器完善支持时,我们已经准备好了。

毕竟,最好的代码不是用最新语法写成的,而是用最合适的工具解决实际问题的。模块就是C++给我们的一把新工具——锋利、高效,但需要时间去掌握它的正确握法。

Read more

Python与人工智能:从脚本到智能体的工程化跃迁

摘要:本文深入剖析Python在AI时代的核心地位与技术演进路径,从GIL(全局解释器锁)的性能困境出发,探讨异步编程、多进程架构与C扩展的破局之道。通过RAG(检索增强生成)系统架构设计与MLOps(机器学习运维)最佳实践,揭示Python如何从"胶水语言"进化为支撑大模型应用的企业级基础设施。文章提供可落地的代码范式、架构设计原则与生产环境部署方案,旨在帮助开发者构建高可用、可扩展的AI工程体系。 引言:Python的"甜蜜负担" 在AI开发领域,Python占据着无可争议的主导地位。据GitHub 2024年度报告显示,Python已连续五年成为AI/ML项目中最常用的编程语言,占比超过68%。然而,这种主导地位背后隐藏着深刻的矛盾:Python的简洁性与AI计算的复杂性之间存在张力,其解释执行特性与生产环境的高性能要求之间存在鸿沟。 许多开发者陷入两个极端:要么沉迷于Jupyter Notebook的快速原型,将"能跑就行"的代码直接投入生产;要么盲目追逐C++/Rust的性能优势,在工程化泥潭中迷失业务价值。真正的Python AI专家,

By Ne0inhk
博主亲测!Python+IPIDEA 自动化高效采集音乐数据

博主亲测!Python+IPIDEA 自动化高效采集音乐数据

文章目录 * 一、前言 * 二、全面认识 * 2.1 初步认识 * 2.2 实际使用感受 * 三、手把手教你:从0到1的完整流程 * 四、实战体验 * 五、超多场景预设,助力解决难题 * 六、用后感受 一、前言 最近想做个某云音乐每日推荐歌单存档小工具 —— 每天自动获取推荐歌曲,存成 Excel 方便回顾。结果刚跑了 3 天,代码就报网络异常,手动访问发现被平台限制了:刷新 10 次有 8 次跳验证,根本拿不到数据。 我一开始没当回事,试了两种办法:先是用免费代理池,结果要么失效快,要么访问速度比蜗牛还慢,歌单同步成功率不到 30%;后来手动换手机热点,每天要切 3 次

By Ne0inhk

python,读取图像文件并获取到像素数组的效率测试

场景需求:读取一个bmp图像文件,并把像素数据转换为RGB*uint8的numpy数组,这是一个很常用的操作。本次测试的目的是使用不同的读取方式,找到其中效率最高的。 素材:demo.bmp,4624*3742像素,RGB格式。 方法1:使用opencv直接读取,就可以获取到像素的数组 import cv2 from PyQt5.QtCore import QElapsedTimer # 创建定时器,统计用时 timer = QElapsedTimer() timer.start() img = cv2.imread("demo.bmp") print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img.shape) 计时结果: 读取bmp文件并转换为数组的时间: 54

By Ne0inhk
Python快速落地的临床知识问答与检索项目(2025年9月教学配置部分)

Python快速落地的临床知识问答与检索项目(2025年9月教学配置部分)

项目概述与技术选型 本项目定位为临床辅助决策支持工具,而非替代临床诊断的独立系统,旨在解决医疗行业两大核心痛点:一是医学知识更新速率加快,2025 年临床指南年均更新量较 2020 年增长 47%,传统知识管理方式难以同步;二是科室规范呈现碎片化分布,不同院区、亚专科的诊疗流程存在差异,导致知识检索效率低下。技术路线采用 RAG 知识库 + ChatFlow 多轮对话 + 工具节点对接 的三层架构,通过整合指南文献、临床路径和院内 SOP 文档,满足门诊快速问诊、病房随访问答及科室知识库精准检索需求,最终实现医疗信息可及性提升 30%、基层医生决策效率提高 25% 的核心价值目标[1]。 技术栈选型分析 1. 大语言模型:领域专精与多模态融合 临床知识问答核心模型需兼顾专业性与部署灵活性。2025 年主流选型包括: * Chimed - GPT:基于 Ziya - V2 架构,通过预训练、

By Ne0inhk