告别传统头文件,拥抱C++ Modules!本文提供模块和传统头文件的对比、模块语法解析、模块化工程实践和5个实操练习,助力您提升编译效率,体验模块带来的便利和优势。
原文标题:告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
原文作者:阿里云开发者
冷月清谈:
- 头文件存在多次编译和二次编译缓慢的问题,模块能解决这些问题并带来编译提速。
- 头文件不“卫生”,容易被外部代码影响,模块不受外部代码影响,保证代码的“卫生”和独立性。
- 头文件缺乏封装,模块具有良好的封装性,可以控制哪些符号对外部用户可见。
- **模块语法简介**
- 导出声明(Export Declaration):控制符号可见性,可以通过export关键字将声明暴露给外部。
- 模块声明(Module Declaration):声明模块名称,一个模块可以由多个模块单元组成。
- 接口单元和实现单元:模块单元分为接口单元(只能有一个)和实现单元(可以有多个)。
- **模块化工程**
- 模块化工程具有不同的组织架构和依赖关系,需要构建工具链正确分析依赖关系。
- 提供头文件兼容和改造技巧,如export using declaration和export extern c++,以及简化模块重构的辅助工具。
- **实操练习**
- 提供5个编程任务,涉及模块基础语法、模块编写、模块化工程等内容,可通过github仓库获取代码和答案解析。
怜星夜思:
2、**除了编译速度提升,模块化改造还能带来哪些好处?**
3、**模块化改造对现有项目来说,有哪些需要注意的事项?**
原文内容
阿里妹导读
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
简介
Hello World!
头文件版本
#include <iostream> int main() { std::cout<<"Hello world!"<<std::endl; }
传统的头文件写法采用 #include 预处理器的语法:
1. 在编译之前,预处理器把整份 iostream 文件的内容复制粘贴到代码中。虽然上面的程序看上去很简单,但展开以后的代码可达到 3 万行,约 1MB!(libstdc++ 下测试)。如果我们想要包含整个标准库,展开以后的代码可达 10 万行以上,约 4MB。
模块版本
import std; int main() { std::cout<<"Hello world!"<<std::endl; }
可以看到,只需要 import std;就可以引入整个标准库,无需精确的包含对应的 iostream 功能模块。尽管我们导入了整个标准库,整个编译流程的速度依然非常快,这是因为头文件中的代码已经被预先编译好,导入模块时无需再次编译。
头文件 VS 模块
重复编译导致编译缓慢
头文件的重复编译
#include<string> void split(std::string& str) { //... }
模块的编译提速
缓慢的二次编译
#include <mylib.h> int main() { mylib::cout<<"Heloo!"; }
当我们编写完成以后,才发现打错了字,把 Hello 拼错了。为此,我们需要修改字符串,然后重新编译。
#include <mylib.h> int main() { mylib::cout<<"Hello!"; }
由于我们只修改了字符串的内容,从理想情况下推测,第二次编译应该比第一次编译快得多。这是因为二次编译应该能够利用之前编译好的结果,只需要重新编译 main 函数即可。
模块是可以被独立编译的代码单元
"卫生"与独立性
头文件不够“卫生”,被其他代码影响
-
顺序依赖性:可能需要按特定顺序包含头文件,不然就会使用出错,从而加大了使用难度。
-
妨碍并行编译和预编译:由于被其他代码影响,ABC 不能并行编译,我们也难以独立的预编译头文件(因为只有被 include 以后我们才能确定头文件的上下文),这大大的限制了编译速度。
-
潜在的冲突可能:比如,头文件 A 中定义了一个宏,刚好 B 中用到了这个宏的名字,那么 B 的内容就会意外的被破坏。(典型如 windows 平台下的 max 宏干扰了 std::max 函数)
模块是"卫生"的:不受外部代码影响
封装控制
头文件缺乏封装
模块具有良好的封装
模块语法简介
Export Declaration
export 控制可见性
// Hello.cppm export inline int a; // 导出一个变量 export void foo(); // 导出一个函数声明 void bar(); // 该声明未被导出 export void foo(){…} // 导出一个函数实现 export class A {}; // 导出一个类 export enum B {}; // 导出一个枚举 export namespace my_lib {}; // 导出一个名称空间 export template<typename T> C{}; // 导出模板 export using std::max; // 导出using declaration export using D=std::vector<int>; // 导出别名 // main.cpp import Hello; int main() { foo(); //使用模块中被导出的声明 // bar(); 编译错误!无法使用未被导出的声明! }
无法被 export 的情形
使用不可见的声明
// Hello.cppm struct my_string { //... }; export my_string hello(); export void hi(const my_string&);
// main.cpp
import hello;
int main() {
// my_string str = hello(); 不能这样写!因为my_string是不可见的
auto str = hello(); // 然而,我们可以通过自动类型推导来匿名的使用不可见类型
hi(str);
}
Module Declaration
module Foo; //声明一个名为Foo的模块 export module Foo.Bar; //声明一个名为Foo.Bar的模块 module Foo.Bar.Gua; //声明一个名为Foo.Bar.Gua的模块
需要注意的是,模块名字中的 . 符号,在语言角度上没有任何特殊的语义,不保证 Foo.Bar 和Foo 之间有从属关系,需要依赖用户来自己组织管理。
接口单元与实现单元
// Foo.cppm export module Foo; // interface unit // Foo_impl1.cpp module Foo; // implementation unit // Foo_impl2.cpp module Foo; // implementation unit
通常来说,在模块中,用户不需要像传统的头文件工程那样,将声明与实现分离开来。然而,依然存在一些特殊情况,例如,对于汇编,我们可能希望分离声明与实现,并通过构建工具在不同平台上选择不同的实现。
// Interface.cppm // Interface Unit export module thread; class thread_context; void switch_in(thread_context* to); void switch_out(thread_context* from); // Impl.cpp // Implementation Unit module thread; class thread_context; { //define something } void switch_in(thread_context* to) { //do something } void switch_out(thread_context* from) { //do something }
例如上述代码,它模拟了一个线程上下文切换的情况。
模块分区
export module Foo.Bar:part1; //声明属于Foo.Bar模块的一个分区(Partition)
export module Foo.bar:part1:part2; // 不合法!分区不能嵌套!
module Foo.Bar:part1; //分区也具有Interface Unit和Implement Unit的区别
Import Declaration
// main.cpp import std; // 导入标准库模块 import foo; // 导入foo.bar模块 import foo.bar:part1; // 不合法!不能导入一个其他模块的分区模块 // foo.cppm export import foo.bar; // 导入foo.bar模块, 并将其再导出给外部用户 import std; // 导入std模块, 但是不暴露给外部用户 // foo.bar.cppm export import :part1; // 导入自己的分区模块part1,并暴露给外部用户
模块代码的基本结构
/*------------Global Module Fragment ------------*/ module; #include "util.hpp" //在模块代码中包含头文件 /*-------------Module Declaration----------------*/ export module http.client; // 本模块的名字叫http.client /*-------------Import Declaration----------------*/ import std; // 导入其他模块 import asio; export import cppjson; import openssl; /*------------------用户代码----------------------*/ namespace http { namespace detail { class helper // 无export前缀,不会暴露给外部用户 { //……… } } // Export Declaration export enum class status { OK, NotFound, //……… }
export class client
{
tcp::socket soc;
//………
};
export int foo();
}
如上述代码所示,模块代码可以分为若干段。
Header Unit
import <iostream>; // 和#include <iostream>的效果完全相同。 // 会引入宏,iostream中的代码也可能会被宏影响 int main() { std::cout<<"Hello world"<<std::endl; }
小结
这里总结一下模块单元的类别:
1. Interface Unit:一个模块只能有一个 Interface,在这里声明哪些符号暴露给外部用户
2. Implementation Unit:一个模块可以有多个 Implementation Unit,可以这里实现代码。
3. Partition Unit:一个主模块可以包含多个分区单元,分区单元不是独立的,无法单独导出给模块的外部用户使用,是主模块的一部分。需要注意的是,分区单元内部也可以划分接口单元和实现单元,这两者是正交的(因此有 2*2=4 种情况)
模块化工程
1. 模块化工程具有和传统头文件不同的组织架构。
2. 各模块之间具有依赖关系,这要求构建工具链能够正确的分析各个模块之间的依赖关系,得到正确的构建顺序。
模块化项目的组织与依赖关系
基于模块的 C++ 工程架构
Module Wrapper
export using declaration
// iostream.cppm module; #include<iostream> export module iostream; namespace std { export using std::cin; export using std::cout; export using std::endl; } // main.cpp import iostream; int main() { std::cout<<"Hello Module Wrapper"<<std::endl; }
在上面的代码中,通过在 global module fragment 中包含标准库头文件,再使用 export using declaration 将这些声明导,这样,我们就制作了一个简单的标准库模块。
async_simple 库链接:
export extern c++
// hello.hpp #ifdef HELLO_USE_MODULE // 通过这个宏来控制是否启用模块 import std; #else #include<iostream> #include<vector> #endif void hello() { // ... } // hello.cppm module; export module hello; export extern "C++" { #define HELLO_USE_MODULE #include<hello.hpp> }
首先我们修改原来的头文件,在 hello.hpp 中,通过 HELLO_USE_MODULE 这个控制宏,来控制是使用模块还是头文件。
模块重构辅助工具
-
自动插入宏,控制项目使用头文件还是模块。
-
自动扫描头文件之间的依赖关系(虽然不太准确)。
-
自动生成 module wrapper 文件。
详见 clang-modules-wrapper:
项目改造经验
async_simple 模块化改造
Hologres 模块化改造
Alibaba Cloud Compiler
更多信息请见说明文档:
代码实操:Workshop
编译环境准备:
2. 安装 Xmake 构建工具:
3. 从 github 上下载 workshop 的操作指引,代码和答案解析详见代码仓库地址:
WorkShop 1:GoodBye Head File,链接见下:
WorkShop 2:Hello world,C++modules,链接见下:
WorkShop 3:编写单个模块,链接见下:
WorkShop 4:编写多模块单元链接,链接见下:
WorkShop 5:将传统的头文件项目转换为模块,链接见下:
回放链接: