本篇文章同步发布于 blog
blog 好读版
前言
Modules 的好处
以往,假如你在一个 cpp file 中 #include
了某个 header file,Preprocessor 会把你要的 header file 引入,变成一个 translation unit
,
但如果你有多个档案都 #include
同一个 header,那 Preprocessor 就会每一个都引入一遍,造成编译速度缓慢。
在引入 C++20 Modules
之后,编译好的 modules 可以直接在各个地方被 compiler 利用,编译速度就可以大幅提升。
当然,Modules 带来好处不只编译速度的提升,还有封装、引入顺序不影响 macro 等优点。
Compiler 支持状况
https://en.cppreference.com/w/cpp/compiler_support
C++20 features > Modules
至目前为止,只有 MSVC 的支持最完整,GCC , Clang 都只有 partial,因此我们这里就以 MSVC 举例。
配置 Visual Studio
在开始之前
在开始之前,我们需要设定一下 Visual Studio,
https://en.cppreference.com/w/cpp/compiler_support
C++23 library features > Standard Library Modules
由于 Standard Library Modules 在 C++23 才引入,
为了处理旧有的 header,我们可以使用 header units
( 其实 msvc 有预先实作,但 IntelliSense 等方面还有些问题,有兴趣的人可以自行尝试 )
Prerequisites
Visual Studio 2019 16.10 or later,这里我直接用 Visual Studio 2022
Project Properties
C++ Language Standard 至少要设定 /std:c++20
Configuration Properties > General > C++ Language Standard
Scan Sources for Module Dependencies 设定成 Yes
Configuration Properties > C/C++ > General
副档名部分各家要求不太一样,Visual Studio 要求的是 Example.ixx
,
你可以直接在 solution explorer 中加入
概述
这里先举个宣告跟实现合在一起的例子
// hello.ixxexport module helloworld; // module declarationimport <iostream>; // import declarationexport void hello() // export declaration{ std::cout << "Hello world!\n";}
// main.cppimport helloworld; // import declarationint main(){ hello();}
// ConsoleHello world!
hello.ixx
export module helloworld;
宣告并汇出一个 module , 名字为 helloworld ,
加上 export
代表这是一个 primary module interface unit
import <iostream>;
以 header unit
的方式 import
一个 header
export void hello();
要汇出一个 function , 在宣告前加上 export
即可
main.cpp
import helloworld;
import
一个 module , 名字为 helloworld, module 的名字为前面 export module
的名称,与档名无关Module declarations - 宣告 Module
这里我们稍微修改一下前一个例子
// hello.ixxexport module helloworld; // module declarationimport <iostream>; // import declarationexport void hello(); // export declaration
// hello.cppmodule helloworld; // declares a module implementation unit for named module 'helloworld'// export void hello() // ERROR: a declaration can be exported only from a module interface unitvoid hello() // implementation for 'void hello()'{ std::cout << "Hello world!\n";}
// main.cppimport helloworld; // import declarationint main(){ hello();}
Module 可以分为以下几种 module units,
module interface unitmodule implementation unitprimary module interface unitmodule partition interface unitmodule partition implementation unitmodule interface unit
module interface unit ,指的是 helloworld.ixx
,
在 module declaration 前有加上 export
,表示其为 interface
,相似于之前的 .h
,
负责宣告、导出 module name , namespaces , functions 等你想导出的东西,
module 的名字为 export module module-name
的 module-name 所定义,与档名无关
module implementation unit
module implementation unit ,指的是 hello.cpp
,
在 module declaration 前没有 export
,表示其为 implementation
,相似于之前的 .cpp
,
负责实现 interface file 中的宣告,如同前面所说,名字为 module-name 所定义,与档名无关。
注意,在
implementation
中也可以进行import
,但export
只能在interface
中使用。
module partition units
module partition interface unit , module partition implementation unit ,
可以将单个 module 分成多个 partition
,后面会再谈到,基本性质跟前面大同小异
primary module interface unit
primary module interface unit ,就这个例子来说,helloworld.ixx
符合其定义,
module 可能会因为 partition
的关係有多个 interface ,
但 primary module interface 每一个 module 都只能也必须有一个,
只要他不是宣告为 module partition ( export module A:B;
),那就会被视为 primary module interface,
dot 句点
举个例子来说,mymodule.mysubmodule
看到 dot,大家直觉应该都觉得是类似 class,
但在 module 中,dot 并没有特殊的含义,就只是名称的一部分,
然而,通常还是会把他拿来表示阶层的关係
Exporting declarations - 汇出宣告
export
除了可以用来汇出 module (export module A;
) 外,也可以汇出宣告跟 namespace,
如果不想每个都打 export
,也可以把东西放在 { }
里,再全部 export
。
// hello.ixxexport module helloworld; // declares the primary module interface unit for named module 'helloworld'// zero() will be visible by translations units importing 'helloworld'export int zero() { return 0; }// one() will NOT be visible.int one() { return 1; }// Both two() and three() will be visible.export{ int two() { return 2; } int three() { return 3; }}// Exporting namespaces also works: number::four() and number::five() will be visible.export namespace number{ int four() { return 4; } int five() { return 5; }}
Importing modules and headers - 汇入 modules 跟 headers
modules and headers
import
大致可以分为两种,一种是一般的 import module
,另一种是前面提到的 import <headeer>
,
摆放的位置,要在 module declaration 后,在其他 declarations 前。
在 module 中,不应该直接使用
#include
,如果要用,要把它放在global module fragment
export-import
透过 module,可以避免我们在 module units 中 import
的东西,被使用 module 的人连带 import
,
但如果你想,可以透过 export
将其再次汇出。
// A.ixx (primary module interface unit of 'A')export module A; import <iostream>; // import headeerexport import <string_view>; // export-imports export void print(std::string_view message){ std::cout << message << std::endl;}
// main.cppimport A; // import moduleint main(){ std::string_view message = "Hello, world!"; print(message);}
Global module fragment
如果因为 macros
的关係需要用到 #define
来设定 headers
,我们可以把它放在 global module fragment
里。
位置在 module 的最开始,第一个宣告必须放 module;
,範围到 module declaration 结束。
// A.ixx (primary module interface unit of 'A')module; // start // https://learn.microsoft.com/en-us/cpp/c-runtime-library/math-constants?view=msvc-170// Defining _USE_MATH_DEFINES to use Math Constants#define _USE_MATH_DEFINES // for C++#include <cmath> export module A; // endimport <iostream>;export void pi(){ std::cout << M_PI << std::endl; // 3.1415926}
Private module fragment
如果你想把宣告跟实现在单个档案中完成,你也可以选择把实现放在 Private module fragment
。
位置在 module 的最尾端,範围由 module : private;
开始。
export module A; export int one(); module : private; // The start of the private module fragment.int one() { return 1;}
Module partitions - 模组分区
最后这部分稍微有点複杂,
如同字面的意思,单个 Module 可以拆为多个 partitions
(分区?),
语法为冒号后加名子,export module module-name:part-name
<primary-module-name>-<module-partition-name>
, ( e.g, A-B
, A-C
)一个 module partition 只能属于一个 module,(export module A:B;
属于 A
)module partition 可以被其他 partition import
, 语法为 import :part-name
在 primary module interface unit,除了 implementation,必须 export
所有 interface partitions ,export import :part-name
须
export
所有 interface partitions 的规定是 No diagnostic is required,
所以 compiler 不一定会提醒,请注意
// A.ixxexport module A; // primary module interface unitexport import :B; // Hello() is visible when importing 'A'.export import :C; // WorldImpl() is visible only for 'A.ixx'.// World() is visible by any translation unit importing 'A'.export void World(){ std::cout << WorldImpl() << '\n';}export int zero(){ return 0;}
// A-B.ixx export module A:B; // partition module interface unit// import :C;import <iostream>;// Hello() is visible by any translation unit importing 'A'.export void Hello() { std::cout << "Hello" << '\n';// std::cout << WorldImpl() << '\n'; // ERROR: WorldImpl() is not visible.}
// A-C.ixxexport module A:C; // partition module interface unit// WorldImpl() is visible by any module unit of 'A' importing ':C'.char const* WorldImpl() { return "World"; }
// main.cpp import A;// import <iostream>;int main(){ Hello(); World(); // std::cout << zero() << '\n'; // ERROR: 'cout': undeclared identifier // WorldImpl(); // ERROR: WorldImpl() is not visible.}
比较特别的是,module partition 一样可以用 implementation unit 的方式
但是要注意,implementation unit 是不能被 export
的,
另外到目前为止,在 MSVC 下,档案的 Compile as 需设定成
Module Internal Partition (/internalPartition )
C/C++ > Advanced > Compile as
// A.cpp export module A; // primary module interface unit import :C; // WorldImpl() is now visible only for 'A.cpp'.// export import :C; // ERROR: Cannot export a module implementation unit.
// A-C.cpp module A:C; // partition module **implementation** unit // WorldImpl() is visible by any module unit of 'A' importing ':C'.char const* WorldImpl() { return "World"; }
结语
Modules 的部分终于暂时写完了,一开始看好像稍嫌複杂,其实不然,只是相同概念的组合
对于 C++20 的主要 feature ,算是大致介绍了一个,
用一篇就写完好像还是太多了? 本来想多切成几篇的说~