一、模块的说明
在前面学习的基础上,再看一下基础模块的定义形式:
c
export(optional) module module-name module-partition (optional) attr (optional) ; (1)
export declaration (2)
export { declaration-seq (optional) } (3)
export(optional) import module-name attr (optional) ; (4)
export(optional) import module-partition attr (optional) ; (5)
export(optional) import header-name attr (optional) ; (6)
module; (7)
module : private; (8)
上述的模块声明使用方法主要有四类:
- 第一类是(1)这种,用来导出相关的模块或模块分区、属性,如下:
c
export module A; // declares the primary module interface unit for named module 'A'
module A; // declares a module implementation unit for named module 'A'
module A; // declares another module implementation unit for named module 'A'
export module A.B; // declares the primary module interface unit for named module 'A.B'
module A.B; // declares a module implementation unit for named module 'A.B'
- 第二类是(2,3)两种,导出模块声明,即将声明或声明序列中的所有名空间和作用域导出,如下:
c
export module A; // declares the primary module interface unit for named module 'A'
// hello() will be visible by translations units importing 'A'
export char const* hello() { return "hello"; }
// world() will NOT be visible.
char const* world() { return "world"; }
// Both one() and zero() will be visible.
export
{
int one() { return 1; }
int zero() { return 0; }
}
// Exporting namespaces also works: hi::english() and hi::french() will be visible.
export namespace hi
{
char const* english() { return "Hi!"; }
char const* french() { return "Salut!"; }
}
- 第三类(4,5,6)这种,导入一个模块单元(分区或头文件等)并重新导出,也就上文中分析的支持聚合接口中的re-export,如下:
c
/////// A.cpp (primary module interface unit of 'A')
export module A;
export char const* hello() { return "hello"; }
/////// B.cpp (primary module interface unit of 'B')
export module B;
export import A;//这种
export char const* world() { return "world"; }
/////// main.cpp (not a module unit)
#include <iostream>
import B;
int main()
{
std::cout << hello() << ' ' << world() << '\n';
}
或这种:
c
/////// A.cpp (primary module interface unit of 'A')
export module A;
import <iostream>;
export import <string_view>;
export void print(std::string_view message)
{
std::cout << message << std::endl;
}
/////// main.cpp (not a module unit)
import A;
int main()
{
std::string_view message = "Hello, world!";
print(message);
}
聚合接口的目的在于,在此模块内导入并导出的模块,既可以在本模块内使用,也可以在引入本模块的其它单元中使用。这才是re-export的精髓。
- 最后一类是最后两种(7,8),也即前面分析的模块的权限,全局模块和私有模块,如下:
c
//全局模块
/////// A.cpp (primary module interface unit of 'A')
module;
// Defining _POSIX_C_SOURCE adds functions to standard headers,
// according to the POSIX standard.
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
export module A;
import <ctime>;
// Only for demonstration (bad source of randomness).
// Use C++ <random> instead.
export double weak_random()
{
std::timespec ts;
std::timespec_get(&ts, TIME_UTC); // from <ctime>
// Provided in <stdlib.h> according to the POSIX standard.
srand48(ts.tv_nsec);
// drand48() returns a random number between 0 and 1.
return drand48();
}
/////// main.cpp (not a module unit)
import <iostream>;
import A;
int main()
{
std::cout << "Random value between 0 and 1: " << weak_random() << '\n';
}
//私有模块
export module foo;
export int f();
module : private; // ends the portion of the module interface unit that
// can affect the behavior of other translation units
// starts a private module fragment
int f() // definition not reachable from importers of foo
{
return 42;
}
注:以上代码来自cppreference
另外,在c++23中,还支持了模块片段,也就是可以随时定义模块分区(前面的例程中其实就是使用的模块片段),大家可以简单理解为更细粒度的模块分区。
模块的命名在前文也已经说明,最好是以物理映射的方式实现模块的描述或声明,比如"abc.test.core.demo",逗号本身并没有具体的意义,只是为了与文件路径表示一致。
在命名的模块中,必须且只有一个模块声明单元没有指定模块分区,一定要注意。这个模块单元被称为主模块接口单元(有点类似于main函数的味道)。
二、模块的分区
所谓的模块分区就是以冒号":"为标志的模块切片,冒号前是模块名称,之后为分区名称。即如下方式定义:
c
export module A:B; // Declares a module interface unit for module 'A', partition ':B'.
它可以被模块聚合形成新的接口导出,方式为:
c
export(optional) import module-partition attr (optional) ;
再看一下模块的分区的例子:
c
/////// A.cpp
export module A; // primary module interface unit
export import :B; // Hello() is visible when importing 'A'.
import :C; // WorldImpl() is now visible only for 'A.cpp'.
// export import :C; // ERROR: Cannot export a module implementation unit.
// World() is visible by any translation unit importing 'A'.
export char const* World()
{
return WorldImpl();
}
/////// A-B.cpp
export module A:B; // partition module interface unit
// Hello() is visible by any translation unit importing 'A'.
export char const* Hello() { return "Hello"; }
/////// 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"; }
/////// main.cpp
import A;
import <iostream>;
int main()
{
std::cout << Hello() << ' ' << World() << '\n';
// WorldImpl(); // ERROR: WorldImpl() is not visible.
}
c++23中的分区片段代码如下:
c
// 单个文件中的模块分区
export module demo;
// 接口分区片段
export module :interface;
export int test(int a, int b);
// 返回主模块
export module demo;
// 实现分区片段
module :impl;
int test(int a, int b) { return a + b; }
三、全局模块和私有模块以及模块内成员区别
在学习了模块及其相关的形式的内容后,对不同的模块及模块分区以及模块内的成员进行一下对比分析:
- 全局模块中的导出成员跨模块可见(和库有些类似);私有模块中的成员则只能本模块内可见;而模块内的普通成员则根据实际的定义属性一确定,看下面的代码:
c
export module demo;
// 1. 全局导出 :外部可见
export void pub_func();
// 2. 私有模块:模块内可见(C++20)
module :private; // 私有分区
void local_func();
// 3. 传统方式:静态为本文件可见
static void in_func();
- 编译处理
在全局模块中,每次修改都会影响模块接口,并进行自动内联,形成一次编译多次使用(模块的优势);而私有模块修改因其私有的情况导致无法影响模块接口;则模块内的函数则仍然依照普通C++的编译模块单元情况进行处理。
模块分区片段中的成员与普通模块中的成员不同主要在于可以由模块对分区内的成员进行更细粒度的export控制。同时,分区片段可以进行增量编译,编译速度会更快。
四、导出控制
在处理导出控制前,先要明白几个关键的术语:
- 聚合模块和子模块
子模块其实就是上面提供到的命名方式中的"a.b.c.d"这种形式中,下一层是上一层的子模块。映射物理文件夹中,子目录中的文件就是父目录中的子模块(这样说是为了直白简单,但不并不完全准确)。
而聚合模块则可以认为是多个子模块组成的模块,比如上面的"a","b"等都是聚合模块。
在实际使用的时候,既可以精确的引用指定的模块,也可以为了方便,一次性引入最顶层的聚合模块。类似于:
c
// demo.ixx :聚合单元声明
export module demo;
// 重新导出所有子模块,提供统一入口
export import demo.core;
export import demo.math;
export import demo.dbus;
export import demo.control;
// 导入demo所有功能
// test.cpp
import demo; // 导入所有子模块
- 命名模块
命名模块很好理解,就是模块有名称,那么就可以通过名称进行控制 - 匿名模块
匿名模块就是没有定义模块的名字,匿名模块需要显示声明并认为是全局模块。匿名模块接近于传统的库的应用方式。见代码:
c
// 头文件:test.h(全局模块)
#pragma once
// 外部可见声明
int add(int a, int b);
// 实现文件:test.cpp
#include "test.h"
// 内部实现
static int in_sub(int a, int b) {
return a - b;
}
// 公共实现
int sub(int a, int b) {
return in_sub(a, b);
}
// 测试代码
#include "test.h" // 包含头文件
int result = sub(6, 3);
在模块的实际开发中,可以通过export关键字和模块分区及片段进行精细化的导出控制和权限处理。通过命名和匿名的模块方式来进行更高层的组织控制;然后通过子模块实现层级级联效果的显式控制和权限设定。
一般来说,对外的接口要在模块的顶层进行显式的控制,确保接口的可控性和一致性;而通过子模块进行级联的内部成员的关系处理,做尽可能的子模块内部的高内聚。
五、总结
通过前面的实例不断的深入,应该对模块有一个较深入全面的学习和理解。本文则通过对学过的技术进行一个整体的总结概括和说明再辅以标准中的相关文档定义和例程,可以掌握模块的整体的说明和相关控制方式。