为什么应该使用模块(Module)替代头文件(Header)?
头文件糟透了!
众所周知,C程序在编译时一般会预处理头文件:
常规解决办法如下:
- LLVM_WHY_PREFIX_UPPER_MACROS
- LLVM_CLANG_INCLUDE_GUARD_H
- template<class _Tp>
- const _Tp& min(const _Tp &__a,
- const _Tp &__b);
- #include <windows.h>
- #undef min // because #define NOMINMAX
- #undef max // doesn’t
但结果依然不够理想,比较一下代码与程序大小你会发现:
另外,头文件形式的可扩展性天生不足。假设有n个源文件,每个源文件引用了m个头文件,那么构建过程的开销会是m×n。这在C++中表现得尤为糟糕。所以预说处理头文件是一个非常糟糕的解决方案。
C家族的模块系统
模块是什么?
- 库的接口(API)
- 库的实现
使用“import”导入已命名的模块:
import会在源文件中忽略预处理状态,并且选择性导入,所以弹性(resilience)非常好。
使用“import”会导入什么?
- 函数、变量、类型、模板、宏,等等;
- 公开API——其它的都隐藏;
- 没有特别的命名空间机制。
C/C++引入模块会怎么样?
引入模块的目标在于:
- 在源文件中指定模块名称;
- API公开;
- 没有头文件!
要编写一个模块非常简单,只需要使用export:
但是这么做会遇到很多遗留问题:
- 需要迁移现在基于头文件的类库;
- 与不支持模块的编译器的互操作性;
- 工具需要理解模块;
所以应该使用引入模块的过渡方案——直接从头文件中构建模块。这么做有以下好处:
- 头文件有利于互操作;
- 程序员不需要完全改变自己习惯的开发模式;
模块地图(Module Map)
模块地图是模块的关键,用来定位模块相关(子)模块,包含以下功能:
- 模块定义命名(子)模块
- 头文件在(子)模块中包含命名头文件的内容
保护伞头文件(Umbrella Header)
- 保护伞头文件会在其目录下包含所有头文件信息
- 使用通配符submodules (module *) 可以为每一个包含的头文件创建一个子模块:
- AST/Decl.h -> ClangAST.Decl
- AST/Expr.h -> ClangAST.Expr
模块编译过程:
- 找到命名模块的module map;
- 产生一个独立编译器实例;
- 在module map中解析头文件。
编辑模块文件过程:
- 在“import”声明处导入模块文件;
- 把模块文件保存在缓存中待重用。
从头文件到模块化
从头文件编程转换到使用模块非常简单:
库方面:合并复合定义的结构、函数、宏,并且为头文件导入依赖,最后编写好模块地图;
开发者方面只需要从“#include”过渡到“import”:
- 把“#inlude”都换成“import”;
- 使用module maps确定(子)模块(类似头文件里的“#include”);
当然,你也可以使用工具来自动化重写代码,非常简单。
工具
编辑性能
使用模块能够提升语法解析性能:
- 模块化的头文件只需要解析一次,之后放在缓存中,于是m×n --> m+n
- 所有基于源(source-based)工具都能带来好处
- 自动链接大大简化了库的使用
- 自动导入可以阻止“#include”带来的可怕的调试结果
调试流
通过DWARF的双程调试有损耗:
- 只能获得“用过”类型和函数
- 丢失了行内函数、模板
另外调试过程还会出现信息冗余
那使用模块的调试又会怎样?
1.提高了构建性能
- 编译器发出的DWARF更少
- 链接器清除重复的DWARF也更少
2.提高了调试体验
- 调试器的ASF精度非常完美
- 调试器不需要寻找DWARF
总结
总而言之,C/C++使用模块化非常有潜力:
- 编译/构建时间的缩短
- 修复各种预处理问题
- 更好的工具体验
- 通过设计,能够平稳地过渡
- Clang实现已经在进行了
这个Slide在Hacker News上引起了激烈讨论,大部分网友还是赞成模块化的方式:
baberman:对我来说没什么不便,而且还给出了过渡方案,可能会很适合某些C/C++项目。我们应该对任何提升C/C++性能的想法持开放态度。
greggman:预处理有什么不好吗?如果我不想用预处理,我完全可以使用Objective-C等。现在的机器性能已经够强大了,import在编译上的性能优势对我来说没有任何吸引力,我更喜欢C/C++的传统方式。
msbarnett反驳greggman:我认为这正是这份提议的精髓所在,你既可以保留使用#include,也可以从现在开始就转向import模块的方式。
_djo_:这个想法会非常有前途!要说缺点的话就是来得太迟了!
nkurz:我不同意“m个头文件+n个源文件 --> m×n倍编译性能”。只要使用“#ifndef _HEADER_H”就不会出现这个问题,或者,为什么不使用#include_once <header.h>来解决呢?预处理很可怕吗?预处理确实有一些问题,但是却是可以克服的。
SeoxyS:我不关心性能,现在性能已经不是问题了,我更关心怎样给开发者更少的负担。
各位CSDN网友是怎样看的呢?欢迎一起讨论。
相关链接:Doug的Slide、Hacker News上的讨论