前言
学习 Vulkan 也有一定时间了,也看一些 C++ 并行编程的书。结合这几个月对图形和 C++ 开源社区关注的体会。感觉现在是一个跨时代的节点,即串行向并行思想转变的时代。尤其是看完 C++ 之父 Bjarne Stroustrup 的论文《Thriving in a Crowded and Changing World: C++ 2006–2020》后,我更坚信我的观点!
想要跟上新时代,就要不断的接受新的思想和新的技术。所以,自己放弃了现在的工作,打算利用一些时间学习一下新技术,新思想。
先从 C++ 20 开始,就以 《Thriving in a Crowded and Changing World: C++ 2006–2020》为基础,系统的学习一下从 C++ 11 到 C++ 20 的主要特性。为迎接 C++ 23 做准备。
- 论文链接
- 中文翻译链接
- 教材:C++20 规范《ISOIEC 148822020》( 自己买的正版,有加密无法分享。虽然不提倡用盗版,但是确实比较贵,一部中低端手机的钱!有兴趣的可以去搜资源 )
- Youtube上找了一个不错的 C++ 20 教学视频(链接)
- 视频源代码地址:https://github.com/rutura/The-C-20-Masterclass-Source-Code
- 自己写的Demo会放到这里: https://gitee.com/thebigapple/Study_CPlusPlus_20_For_CG.git
注意:关于 C++ 规范的内容笔记都是机翻+本人修正的。没有太多借鉴价值。都是长句,很难翻译!翻译是为了大致了解规范的内容,便于定位问题。定位后去详读规范文档。这样才能真正理解大佬设计规范的思想!
Modules
先从 Modules 出发,个人认为它是 C++20 最重要的内容之一。据说 C++ 23 所有标准库都要支持 Modules!
引用
- C++20 规范《ISOIEC 148822020》
规范内容笔记
Module units and purviews
格式
- module-declaration
- export-keyword(可选) + module-keyword + module-name + module-partition(可选)+ attribute-specifier-seq(可选);
- module-name
- module-name-qualifier(限定词)(可选)+ identifier (标识符)
- module-partition
- : + module-name-qualifier + identifier
- module-name-qualifier
- identifier .
- module-name-qualifier + identifier .
内容
- module unit: 是包含 module-declaration 的翻译单元
- 被命名的 module 是具有相同 module 名称的 module unit 集合
- module 和 import 不应作为标识符( identifiers )出现在 module-name 和 module-partition 中
- 任何以 std 后接数字开头,或以保留标识符开头的 module-names 不应在 module 声明中指定。也就是说声明 module 时,不能使用这些保留字段
- 不需要诊断
- module interface unit: 是 module-declaration 以 export-keyword 为开头的 module unit
- 任何其他 module unit 都是 module implementation unit。可以理解为 module interface unit 类似 “.h”,其他都是 “.cpp”
- 定义一个 module 应该只包含一个没有 module-partition 的 module interface unit。称:module 的 primary module interface unit
- 不需要诊断
- module partition: 是一个 module-declaration 包含 module-partition 的 module unit
- 一个被命名的 module,在相同的 module-partition 中不应包含多个 partitions
- 作为 module interface units 的 module 的所有 module partitions 应由 primary module interface unit 直接或间接导出( export )
- 违反这些规则不需要诊断,也就是说这是一个建议。
- Module partitions 只能由同一 module 中的其他 module units 导入。将 module 划分为 module units 在 module 外部是不可见的
- 示例
Translation unit #1: export module A; export import :Foo; export int baz(); Translation unit #2: export module A:Foo; import :Internals; export int foo() { return 2 * (bar() + 1); } Translation unit #3: module A:Internals; int bar(); Translation unit #4: module A; import :Internals; int bar() { return baz() - 10; } int baz() { return 30; }
- 该示例包含四个翻译单元
- 一个 primary module interface unit (#1)
- 一个 module partition (#2 A:Foo)
- 是构成 module A 的接口一部分的 module interface unit
- 一个 module partition ( A:Internals)
- 对 module A 的外部接口没有贡献
- 一个模块实现单元,提供了 bar 和 baz 的定义,因为没有 partition 名称,所以无法被导入
- 该示例包含四个翻译单元
- module unit purview (可见性): 是从 module-declaration 开始,一直延伸到 translation unit 末尾的 tokens 序列
- 被命名的 module M 的 purview,是 module M 的 module units 的 module unit purviews 集合
- global module: 是所有 global-module-fragments 和所有非 module units 的翻译单元的集合单元
- 出现在这种上下文中的声明,被认为是 global module 的 purview 内
- global module 没有名称,没有 module interface unit,也没有被任何 module-declaration 引⼊
- 一个 module 或是被命名的 module 或是 一个 global module
- 声明附加到 module 有如下规则
- 如果要声明到 module 中
- 或是可替换的全局分配和释放函数
- 或是具有外部链接的 namespace-definition
- 或是出现在连接规范(linkage-specification)中
- 声明会附加到 global module
- 如果要声明到 module 中
- 其他情况
- 声明会附加到其 purview 内的 module 中
- 声明附加到 module 有如下规则
- 既不包含 export-keyword 也不包含 module-partition 的 module-declaration 会隐式的导入到 module 的 primary module interface unit 中。就像 module-import-declaration 一样
- 示例
Translation unit #1: module B:Y; // does not implicitly import B int y(); Translation unit #2: export module B; import :Y; // OK, does not create interface dependency cycle int n = y(); Translation unit #3: module B:X1; // does not implicitly import B int &a = n; // error: n not visible here Translation unit #4: module B:X2; // does not implicitly import B import B; int &b = n; // OK Translation unit #5: module B; // implicitly imports B int &c = n; // OK
- 示例
Export declaration
格式
- export-declaration
- export + declaration
- export + { declaration-seq (可选) }
- export-keyword + module-import-declaration
内容
- export-declaration 应仅出现在命名空间范围内,并且仅出现在 module interface unit 的 purview 内
- export-declaration 不应直接或间接出现在未命名的命名空间或 private-module-fragment
- export-declaration 具有其 declaration、declaration-seq(如果存在)、module-import-declaration 的声明效果
- export-declaration 不会设置范围,其 declaration 或 declaration-seq 不应包含 export-declaration 或 module-import-declaration
- 以下情形,声明会被导出
- 在 namespace-scope 声明的 export-declaration
- 包含 export-declaration 的 namespace-definition
- 在头文件中(header unit)至少引入一个名称的声明
- 一个不是 module-import-declaration 的导出声明应声明至少一个名称,如果声明不在头文件中,则不应声明具有内部链接的名称。
-
示例
Source file "a.h": export int x; Translation unit #1: module; #include "a.h" // error: declaration of x is not in the // purview of a module interface unit export module M; export namespace {} // error: does not introduce any names export namespace { int a1; // error: export of name with internal linkage } namespace { export int a2; // error: export of name with internal linkage } export static int b; // error: b explicitly declared static export int f(); // OK export namespace N { } // OK export using namespace N; // error: does not declare a name
-
- 如果声明是 using-declaration 并且不在头文件中,则所有 using-declarators 最终引用的所有实体(如果有)都应使用具有外部链接的名称引入
- 示例
Source file "b.h": int f(); Importable header "c.h": int g(); Translation unit #1: export module X; export int h(); Translation unit #2: module; #include "b.h" export module M; import "c.h"; import X; export using ::f, ::g, ::h; // OK struct S; export using ::S; // error: S has module linkage namespace N { export int h(); static int h(int); // #1 } export using N::h; // error: #1 has internal linkage
- 这些约束不适用于由 typedef 声明和别名声明引入的类型名称
- 示例
export module M; struct S; export using T = S; // OK, exports name T denoting type S
- 示例
- 一个实体的导出声明的重新声明是隐式导出的
- 导出一个实体的非导出声明的重新声明会报格式错误
export module M; struct S { int n; }; typedef S S; export typedef S S; // OK, does not redeclare an entity export struct S; // error: exported declaration follows non-exported declaration
- 导出一个实体的非导出声明的重新声明会报格式错误
- 如果名称由 module 的 purview 内的导出声明引入或重新声明,则该名称由该模块导出
- 导出的名称要么有外部链接,要么没有链接;
- 由 module 导出的 Namespace-scope names 对于导入该 module 的任何翻译单元中的名称查找都是可见的;
- 在可以访问类型定义的任何上下文中,类和枚举成员名称对于名称查找都是可见的
Interface unit of M: export module M; export struct X { static void f(); struct Y { }; }; namespace { struct S { }; } export void f(S); // OK struct T { }; export T id(T); // OK export struct A; // A exported as incomplete export auto rootFinder(double a) { return [=](double x) { return (x + a/x)/2; }; } export const int n = 5; // OK, n has external linkage Implementation unit of M: module M; struct A { int value; }; Main program: import M; int main() { X::f(); // OK, X is exported and definition of X is reachable X::Y y; // OK, X::Y is exported as a complete type auto f = rootFinder(2); // OK return A{45}.value; // error: A is incomplete }
- 在 export-declaration 中重新声明名称不能更改名称的链接
Interface unit of M: export module M; static int f(); // #1 export int f(); // error: #1 gives internal linkage struct S; // #2 export struct S; // error: #2 gives module linkage namespace { namespace N { extern int x; // #3 } } export int N::x; // error: #3 gives internal linkage
- 导出的 namespace-definition 或导出的 linkage- pecification 中的声明被导出并受导出声明规则的约束
export module M; export namespace N { int x; // OK static_assert(1 == 1); // error: does not declare a name }
Import declaration
格式
- module-import-declaration
- import-keyword + module-name + attribute-specifier-seq(可选)
- import-keyword + module-partition + attribute-specifier-seq(可选)
- import-keyword + header-name + attribute-specifier-seq(可选)
内容
-
module-import-declaration 只能出现在全局命名空间范围内
- 在一个 module unit 中,所有 module-import-declarations 和 export-declarations 的导出声明应在 translation-unit 和 private-module-fragment(如果有)的 declaration-seq 中的所有其他声明之前。
- 可选项 attribute-specifier-seq 属于 module-import-declaration
-
module-import-declaration 导入一组按如下所述确定的翻译单元
- 由导入的翻译单元导出的 Namespace-scope names 在导入的翻译单元中变得可见
- 导入的翻译单元内的声明,在导入声明后,对该导入翻译单元可访问(翻译真的绕)
-
指定 module-name 为 M 的 module-import-declaration 导入 M 的所有模块接口单元
-
(4)指定 module-partition 的 module-import-declaration 只能出现在某个模块 M 的模块单元中的 module-declaration 之后
- 这样的声明导入了 M 的所谓模块分区(module partition)
-
指定 header-name H 的 module-import-declaration 导入合成(synthesized )的 header unit,该单元是通过将翻译阶段(Phases of translation)应用于 H 指定的头文件和源文件形成的翻译单元, 这些头文件和源文件不应包含 module-declaration
- header unit 中的所有声明都被隐式导出,并附加到全局 module 中
-
importable header 是实现定义的头文件成员集合,其中包括所有可导入的 C++ 头文件。
- H 应标识一个可导入的头。给定两个这样的 module-import-declarations
- 如果它们的 header-names 标识不同的头文件或源文件,则它们导入不同的 header units
- 否则,如果它们出现在同一个翻译单元中,则它们导入同一个 header unit
- 除此之外的情况,不确定它们是否导入相同的 header unit
- 预处理器也可以识别指定 header-name 的 module-import-declaration,并导致在 header unit 转换的第(4)阶段结束时定义的宏变得可见
- 任何其他 module-import-declaration 都不会使宏可见
- H 应标识一个可导入的头。给定两个这样的 module-import-declarations
-
尽管所有声明都被隐式导出,但在 header unit 中允许声明具有内部链接的名称
- 出现在多个翻译单元中的定义通常不能引用此类名称
- header unit 不应包含名称具有外部链接的非内联函数或变量的定义
-
当一个 module-import-declaration 导入一个翻译单元 T 时,它还导入了 T 中由导出的 module-import-declarations 导入的所有翻译单元;
- 这样的翻译单元被认为是由 T 导出的。
- 此外,当某个 module M 的 module unit 中的 module-import-declaration 导入 M 的另一个 module unit U 时,它还导入由 U 的 module unit purview 内未导出的 module-import-declarations 导入的所有翻译单元
- 这些规则反过来又会导致更多翻译单元的输入
-
不得导出模块实现单元
Translation unit #1: module M:Part; Translation unit #2: export module M; export import :Part; // error: exported partition :Part is an implementation unit
-
不是 module partition 的 module M 的 module 实现单元不应包含指定 M 的 module-import-declaration
module M; import M;
-
如果翻译单元包含导入 U 的声明(可能是 module-declaration),或者如果它有对 U 的接口依赖的翻译单元有接口依赖,则翻译单元对 U 具有接口依赖。
- 翻译单元应对自身没有接口依赖。
Interface unit of M1: export module M1; import M2; Interface unit of M2: export module M2; import M3; Interface unit of M3: export module M3; import M1;
- 翻译单元应对自身没有接口依赖。
Global module fragment
格式
- global-module-fragment
- module-keyword + ; + declaration-seq(可选)
内容
- 在翻译的第 4 阶段之前,只有预处理指令可以出现在声明序列中
- global-module-fragment 指定 module unit 的全局模块片段的内容。
- 全局模块片段可用于提供附加到 global module 并可在 module unit.内使用的声明
- 声明 D 可以从同一翻译单元中的声明 S 中进行 decl-reachable (访问),条件如下:
- D 不声明函数或函数模板,并且 S 包含 id-expression、namespace-name、type-name、template-name、或 concept-name 命名的 D
- D 声明了一个由出现在 S 中的表达式命名的函数或函数模板
- S 包含形式为表达式 E 的 postfix-expression ( expression-list(可选))
- postfix-expression 表示从属名称,或者对于其运算符表示从属名称的运算符表达式,并且 D 通过名称查找找到,以查找从 E 合成的表达式中的相应名称,方法是将每个依赖于类型的参数或操作数替换为一个值没有关联命名空间或实体的占位符类型
- S 包含一个表达式,它获取重载函数的地址,该函数的重载集包含 D 并且目标类型依赖于该函数
- 存在一个不是命名空间定义的声明 M,对于该声明 M 可以从 S 声明访问,并且
- D 可以从 M 声明访问
- D 重新声明 M 声明的实体或 M 重新声明 D 声明的实体,D 既不是友元声明也不是块作用域声明
- D 声明了一个命名空间 N 并且 M 是 N 的成员
- M 和 D 之一声明一个类或类模板 C,另一个声明 C 的成员或友元
- D 和 M 之一声明枚举 E,另一个声明 E 的枚举数
- D 声明一个函数或变量,M 在 D 中声明
- M 和 D 之一声明模板,另一个声明该模板的部分或显式特化或隐式或显式实例化
- M 和 D 之一声明一个类或枚举类型,另一个引入一个 typedef 名称以用于该类型的链接目的
- 在此确定中,未指定
- 对 alias-declaration、typedef 声明、using-declaration 或 namespace-alias-definition 的引用是否被它们在此确定之前命名的声明替换
- 在此确定之前,是否将不表示依赖类型且其 template-name 命名为别名模板的 simple-template-id 替换为其表示的类型
- 在此确定之前,是否将不表示依赖类型的 decltype-specifier 替换为其表示的类型
- 在此确定之前,是否将不依赖值的常量表达式替换为常量评估的结果
- 如果 D不可从 translation-unit 的 declaration-seq 中任何声明访问,则丢弃 module unit 的 global module fragment 中的声明 D
-
即使在实例化上下文 includes module unit 的情况下,丢弃的声明对于 module unit 之外的名称查找,以及在其实例化节点位于 module unit 之外的模板实例化来说都是不可访问或不可见的
-
示例
const int size = 2; int ary1[size]; // unspecified whether size is decl-reachable from ary1 constexpr int identity(int x) { return x; } int ary2[identity(2)]; // unspecified whether identity is decl-reachable from ary2 template<typename> struct S; template<typename, int> struct S2; constexpr int g(int); template<typename T, int N> S<S2<T, g(N)>> f(); // S, S2, g, and :: are decl-reachable from f template<int N> void h() noexcept(g(N) == N); // g and :: are decl-reachable from h
Source file "foo.h": namespace N { struct X {}; int d(); int e(); inline int f(X, int = d()) { return e(); } int g(X); int h(X); } Module M interface: module; #include "foo.h" export module M; template<typename T> int use_f() { N::X x; // N::X, N, and :: are decl-reachable from use_f return f(x, 123); // N::f is decl-reachable from use_f, // N::e is indirectly decl-reachable from use_f // because it is decl-reachable from N::f, and // N::d is decl-reachable from use_f // because it is decl-reachable from N::f // even though it is not used in this call } template<typename T> int use_g() { N::X x; // N::X, N, and :: are decl-reachable from use_g return g((T(), x)); // N::g is not decl-reachable from use_g } template<typename T> int use_h() { N::X x; // N::X, N, and :: are decl-reachable from use_h return h((T(), x)); // N::h is not decl-reachable from use_h, but // N::h is decl-reachable from use_h<int> } int k = use_h<int>(); // use_h<int> is decl-reachable from k, so // N::h is decl-reachable from k Module M implementation: module M; int a = use_f<int>(); // OK int b = use_g<int>(); // error: no viable function for call to g; // g is not decl-reachable from purview of // module M’s interface, so is discarded int c = use_h<int>(); // OK
-
Private module fragment
格式
- private-module-fragment
- module-keyword : private + ; + declaration-seq(可选)
内容
- private-module-fragment 应仅出现在 primary module interface unit 中
- 有 private-module-fragment 的 module unit 应是其 module 的唯一 module unit
- 不需要诊断
- private-module-fragment 结束模块接口单元中可能影响其他翻译单元行为的部分。
- private-module-fragment 允许将 module 表示为单个翻译单元,而不会使 module 的所有内容都可供导入器(importers)访问
- private-module-fragment 的存在会影响
- 需要定义导出的内联函数的节点
- 需要定义具有占位符返回类型的导出函数的节点
- 是否要求声明不是风险暴露
- 必须出现内联函数和模板的定义
- 在它之前实例化的模板的实例化上下文
- 其中声明的可访问性
export module A; export inline void fn_e(); // error: exported inline function fn_e not defined // before private module fragment inline void fn_m(); // OK, module-linkage inline function static void fn_s(); export struct X; export void g(X *x) { fn_s(); // OK, call to static function in same translation unit fn_m(); // OK, call to module-linkage inline function } export X *factory(); // OK module :private; struct X {}; // definition not reachable from importers of A X *factory() { return new X (); } void fn_e() {} void fn_m() {} void fn_s() {}
Instantiation context
内容
- 实例化上下文是程序中的一节点,用于确定在特定声明或模板实例化的上下文中哪些名称对依赖于参数的名称查找可见,哪些声明可访问
- 在默认函数的隐式定义期间,实例化上下文是类定义中的实例化上下文与导致默认函数的隐式定义的程序构造的实例化上下文的联合
- 在其实例化节点被指定为封闭特化的模板的隐式实例化期间,实例化上下文是封闭特化的实例化上下文的联合,并且如果模板在模块接口中定义模块 M 的单元和实例化节点不在 M 的模块接口单元中,位于 M 的主模块接口单元的声明序列末尾的节点(在私有模块片段之前,如果有的话)
- 在隐式实例化模板的隐式实例化期间,因为它是从默认函数的隐式定义中引用的,所以实例化上下文是默认函数的实例化上下文
- 在任何其他模板特化的实例化期间,实例化上下文包括模板的实例化节点
- 在任何其他情况下,程序内某个节点的实例化上下文包括该节点
- 示例
Translation unit #1: export module stuff; export template<typename T, typename U> void foo(T, U u) { auto v = u; } export template<typename T, typename U> void bar(T, U u) { auto v = *u; } Translation unit #2: export module M1; import "defn.h"; // provides struct X {}; import stuff; export template<typename T> void f(T t) { X x; foo(t, x); } Translation unit #3: export module M2; import "decl.h"; // provides struct X; (not a definition) import stuff; export template<typename T> void g(T t) { X *x; bar(t, x); } Translation unit #4: import M1; import M2; void test() { f(0); g(0); }
- 对
f(0)
的调用是有效的;foo<int, X>
的实例化上下文包括- 翻译单元 #1 末尾的节点
- 翻译单元 #2 末尾的节点
- 调用
f(0)
的节点
- 所以 X 的定义是可访问的
- 未指定对
g(0)
的调用是否有效:bar<int, X>
的实例化上下文包括- 翻译单元 #1 末尾的节点
- 翻译单元#3末尾的节点
- 调用
g(0)
的节点
- 所以 X 的定义不需要是可访问的
- 对
Reachability
内容
-
如果 U 是包含 P 的 translation unit 具有接口依赖关系的 module interface unit,或者包含 P 的 translation unit 在 P 之前导入 U,则 translation unit U 必须从节点 P 访问
- 虽然 module interface units 是可访问的,即使它们仅通过非导出导入声明传递导入,来自此类 module interface units 的 namespace-scope names 对于名称查找不可见
-
所有必须可访问的翻译单元都是可访问的。程序中的节点具有接口依赖关系的其他翻译单元可能被认为是可访问的,但未指定哪些是以及在什么情况下
- 避免依赖于打算移植的程序中任何其他翻译单元的可访问性
-
如果对于实例化上下文中的任何节点 P,声明 D 是可访问的,则
- 在同一翻译单元中 D 出现在 P 之前
- D 未被丢弃, 出现在可从 P 访问的翻译单元中,并且未出现在 private-module-fragment 中
- 声明是否导出与是否可访问无关
-
上下文中实体的所有可访问声明的累积属性决定了该上下文中实体的行为
- 这些可访问语义属性包括类型完整性、类型定义、初始化器、函数或模板声明的默认参数、属性、类或枚举成员名称对普通查找的可见性等。由于默认参数是在调用表达式的上下文中计算的,因此相应参数类型的访问语义属性适用于该上下文
-
示例
Translation unit #1: export module M:A; export struct B; Translation unit #2: module M:B; struct B { operator int(); }; Translation unit #3: module M:C; import :A; B b1; // error: no reachable definition of struct B Translation unit #4: export module M; export import :A; import :B; B b2; export void f(B b = B()); Translation unit #5: module X; import M; B b3; // error: no reachable definition of struct B void g() { f(); } // error: no reachable definition of struct B
-
即使名称查找不可见,实体也可以具有可访问的声明
-
示例
Translation unit #1: export module A; struct X {}; export using Y = X; Translation unit #2: module B; import A; Y y; // OK, definition of X is reachable X x; // error: X not visible to unqualified lookup
学习笔记
引用
非常推荐阅读的
- 关于 Modules 的 Talk
- https://www.youtube.com/watch?v=tjSuKOz5HK4
- 大佬的讲座,看完后,结合 c++ 20 的规范,就能能很容易理解 Modules
- Modules 的教程
- https://www.youtube.com/watch?v=ow2zV0Udd9M
- 主要讲如何编写 Modules 的代码。
- 这个视频的 Code 非常值得去深入研究,刚自己实现的时候没觉得这个 Demo 有什么特别。当我在图书馆午休的时候,半梦半醒的状态下,回忆起了这段代码,才突然发现,这段代码的价值。立马打开电脑仔细读了一下!
- 其中一点时可以利用 C++ 新特性变参模板、variant 和 visit 实现多态
- gcc 关于 Modules 的文档
- https://gcc.gnu.org/wiki/cxx-modules
- 利用主流编译器编译 Modules 我都做过实验了, 最后我选择了 gcc。所以 gcc 的文档还是很有价值的
其他比较好的引用
- 微软对 Modules 的概述
- https://docs.microsoft.com/en-us/cpp/cpp/modules-cpp?view=msvc-170
- https://docs.microsoft.com/en-us/cpp/cpp/tutorial-named-modules-cpp?view=msvc-170
- 不错的资料,有 Moudles 的示例代码
- https://www.codeproject.com/Articles/1214398/Modules-for-Modern-Cplusplus
- VS 为 Modules 添加一个 “ixx” 后缀
- https://marvinsblog.net/post/2020-11-14-msvc-cpp-modules/
Modules 个人总结
Modules 与 传统 C++ 代码
-
传统 c++ 代码
- 一般我们编写项目代码时,会把代码分为三种类型:
- 公共头文件
- 统一包含需要暴露给外部的头文件
- 头文件
- 一般都是数据结构定义和接口函数定义
- 源文件
- 具体数据结构的实现和函数的实现
- 公共头文件
- 代码如下:
公共头文件 #0: #ifndef STRING_H #define STRING_H #define STRING_INCLUDED #include "SPString.h" #undef STRING_INCLUDED #endif ------------------------------------ 头文件 #1: #pragma once #include <string> using sp_string = std::string; class SPString { public: virtual bool is_empty() = 0; virtual const sp_string& get_string() = 0; }; SPString* Make_String(); ------------------------------------ 头文件 #2: #pragma once #include "SPInterfcae.h" class SPStringInternal : public SPString { public: virtual bool is_empty() override; virtual const sp_string& get_string() override; private: sp_string str_; }; ------------------------------------ 源文件 #3: #include "SPStringInternal.h" SPString* Make_String() { return new SPStringInternal(); } bool SPStringInternal::is_empty() { return false; } const sp_string& SPStringInternal::get_string() { return str_; } ------------------------------------
- 一般我们编写项目代码时,会把代码分为三种类型:
-
Modules (对比)
- Modules 也可以按照上面的分类方式总结
- 公共头文件对应 Modules 的 primary module interface unit
- 文件对应 Modules 的 module interface unit
- 源文件对应 Modules 的 module implementation unit
- 代码如下:
primary module interface unit #0 export module SPString; export import :String; ------------------------------------ module interface unit #1: module; #include <string> export module SPString:String; export using sp_string = std::string; export class SPString { public: virtual bool is_empty() = 0; virtual const sp_string& get_string() = 0; }; export SPString* Make_String(); ------------------------------------ module interface unit #2: export module SPString:String.Internal; import :String; class SPStringInternal : public SPString { public: virtual bool is_empty() override; virtual const sp_string& get_string() override; private: sp_string str_; }; ------------------------------------ module implementation unit #3: module SPString; import :String; import :String.Internal; SPString* Make_String() { return new SPStringInternal(); } bool SPStringInternal::is_empty() { return false; } const sp_string& SPStringInternal::get_string() { return str_; }
- Modules 也可以按照上面的分类方式总结
-
我是按照这种方式进行划分的,如果觉得这样不合理,欢迎指正!
-
调用方式
- 传统 c++ 代码
#include "SPString.h" auto main() -> int { auto str = Make_String(); str->get_string(); return 1; }
- Modules
import SPString; auto main() -> int { auto str = Make_String(); str->get_string(); return 1; }
- 传统 c++ 代码
-
传统 c++ 风格的不足之处
- 不够简洁
- 一个头文件中的代码可能会影响同一翻译单元中包含的另一个
#include
中的代码的含义 #include
顺序相关- 宏全局可见
- 我的感受
- 在开发中如果引入头文件不规范时,很难溯源,根本找不到某个方法在哪里被包含进来的
- 有的时候,头文件引入的顺序有问题,也会导致编译不通过
- 一个头文件中的代码可能会影响同一翻译单元中包含的另一个
- 分离编译的不一致性
- 两个翻译单元中同一实体的声明可能不一致,但并非所有此类错误都被编译器或链接器捕获、
- 编译次数过多 (最核心问题)
- 从源代码文本编译接口比较慢。从源代码文本反复地编译同一份接口非常慢
- 不够简洁
-
Modules 的优势
- 减少编译时间
- 防止各种代码项之间的相互依赖
- 避免使用库时存在的头文件之间的混淆
-
Modules 不足(个人最近使用总结的观点)
- 各大编译厂商对 Modules 的支持还不够完备
- 虽然所有编译器都能编译 Modules 的代码,但是涉及到动态链接库和静态链接库的时候想要成功编译,还需要设置复杂的参数
- IDE 支持还不够
- 虽然 Visual Studio 支持智能提示,但我还是没办法使用动态链接的方式编译项目(估计是还没找到相关文档)。
- 其他 IDE 暂时还没有完全支持 Modules 的智能提示
- 总的来说,可以使用 Modules 进行开发,但是由于 IDE 还没有充分支持,开发效率有限
- 各大编译厂商对 Modules 的支持还不够完备
Global Module Fragment 和 Private Module Fragment
- 具体解释可以参照规范小节,这里只记录一下自己的理解
- global module fragment
- 必须出现在所有 module unit 开头 (可以没有 global module fragment)
- 形式:
module;
- 形式:
- 作用:
- 个人理解就是为了兼容 old style 的库
- 所有
#include
必须放置在module;
之后export module ***;
之前 - 这是就可以调用包含进来的函数
- 必须出现在所有 module unit 开头 (可以没有 global module fragment)
- private module fragment
- 必须出现在 primary module interface unit 中
- 形式:
module: private;
- 个人感觉这个关键字是为了那些设计只有头文件库的专家使用的
- 如:stb_image 这类
- 形式:
- 需要出现在整个 Module 的末尾
- 必须出现在 primary module interface unit 中
Visual Studio Modules 开发环境笔记
-
Modules 支持设置
- 下载最新的 Visual Studio 安装
- 下载最新的 Windows SDK 安装
- 创建工程
- 右键单击工程 -> 属性 -> 配置属性 -> C/C++ -> 语言 -> c++ 语言标准
- 设置成 C++ 20 或者更高,或 latest
- 右键单击工程 -> 属性 -> 配置属性 -> C/C++ -> 语言 -> 启用实验性的 C++ 标准模块
- /experimental:module
-
注意事项
- 传统的动态链接和静态链接需要头文件,但是 Module 是没有头文件的。因此,链接方式不同
- 静态链接方式
- 目前我只找到一种方式
- 右键单击工程->引用
- 添加引用
- 添加对应的需要链接的工程
- 目前我只找到一种方式
- 动态连链接的方法没有找到。如果有知道方法的小伙伴可以教一下我,多谢!
- 开发时建议将所有 module interface unit 放在一个文件夹里。
- 将 primary module interface unit 和 module interface unit 分开放可能在编译时,出现不能找到某个 Module 的情况
- Visual Studio 虽然支持 Modules 的开发,但还是有很多问题的
Visual Studio Code + GCC Modules 开发环境笔记
-
环境搭建
- 下载安装 VS Code
- 编译器支持文档 (Google 搜索: c++ compiler support)
- https://en.cppreference.com/w/cpp/compiler_support
- MinGW (Google 搜索: winlibs)
- https://winlibs.com/
- 选择下载:Release versions -> GCC 12.2.0 -> Zip archive 下载
- 解压安装在 C 盘根目录
- 改名为 mingw
- 将 C:\mingw64\bin 添加到环境变量
- 命令行 gcc --version
- 命令行 clang++ --version
- 命令行 gdb --version
- 安装 VS Code 扩展
- c/c++
-
gcc 编译注意事项
- tasks.json 中 args 需要添加:
- “-std=c++20” or 更高
- “-fmodules-ts”
- gcc 编译 Module 会生成一个 gcm.cache 的文件夹,该文件夹生成路径暂时很难修改,需要写 Module Mapper
- 这个支持还不完善,所以我自己写了一个复制脚本
- Module Mapper 官方说明地址
- https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Module-Mapper.html
- gcm.cache 存放的是 *.gcm 的文件,静态链接和动态链接库,需要该文件映射。
- 个人理解有点像头文件
- 如果某个项目需要引入使用 Modules 生成的静态或者动态链接库,就需要拷贝这个库的 primary module interface unit 生成的 *.gcm 文件到该项目中的 gcm.cache 文件夹里。否则,项目编译不会通过。
- 暂时没有很好的自动拷贝 *.gcm 的有效方法或者设置 gcm.cache 文件夹路径的方法
- tasks.json 中 args 需要添加:
Visual Studio Code + CMake + GCC Modules 开发环境笔记
-
环境搭建
- VS Code 和 GCC 略
- 安装 Cmake 扩展
- Cmake
- Cmake Tool
-
创建 CMake 工程
- 创建空文件夹
- 查看 -> 命令面板 -> cmake quick start -> gcc -> 输入项目名称 -> 可执行
-
Cmake 构建和编译注意事项
- 支持 Modules 需设置
- set(CMAKE_CXX_STANDARD 20) 或更高
- SET(CMAKE_CXX_FLAGS “-fmodules-ts”)
- 同样 gcm.cache 中的 *.gcm 需要使用脚本去拷贝
- 支持 Modules 需设置
总结
我个人是特别喜欢 Modules 的,毕竟解决我开发中三个最大的痛点
- 第一,混乱的头文件的包含
- 开发中,很多人包含头文件非常随意,导致有些不应该暴露给特点对象的 code 被该对象使用。
- 而 import 只对当前 Modules 有效,避免上述情况发生
- 第二,宏的滥用
- 我个人感觉,现代 C++ 新特性好像在告诉我们,以后要少用宏,或者不同宏!
- Moudules 和 新的标准库模块(如 File)就已经能减少一小半的宏的使用
- 还有 inline 和 constepxr 也是规范我们,尽量使用 inline 函数 和 constepxr 关键字来代替宏
- 我个人感觉,现代 C++ 新特性好像在告诉我们,以后要少用宏,或者不同宏!
- 第三,也是我的最大痛点
- 我是一个以调试为基础的开发者,特别喜欢快速原型这个模型。因此,开发时,会建立很小的里程碑,调试,通过,再建里程碑…,如此反复。
- 因此,代码的编译,会占用我很多的开发时间。使用 Modules 比传统编译模式要快 10 到 20 倍,这大大加快了我的开发节奏!