一,模块的说明
如前,再看一下基础模块的定义形式:
cpp
export(可选) module 模块名 模块分块 (可选) 属性 (可选) ; (1)
export declaration (2)
export { 声明序列 (可选) } (3)
export(可选) import 模块名 属性(可选) ; (4)
export(可选) import 模块分块 属性(可选) ; (5)
export(可选) import 头名 属性(可选) ; (6)
module; (7)
module : private; (8)
上述的模块声明用法主要有四类:
第一类是(1)该,用来导出私有模块或模块分块,属性,如下:
cpp
export module A;
//对`"A"`命名模块,声明主要`模块接口`单元
module A;
//对`"A"`命名模块,声明模块实现单元
module A;
//对`"A"`命名模块,声明`另一个`模块实现单元
export module A.B;
//对`"A.B"`命名模块,声明主要`模块接口`单元
module A.B;
//对`"A.B"`命名模块,声明模块实现单元
第二类是(2,3)两个,导出模块声明,即将声明或声明序列中的所有名字空间和域导出,如下:
cpp
export module A;
//对`"A"`命名模块,声明主要`模块接口`单元
//导入`'A'`的`翻译单元`可看到`hello()`单元
export char const* hello() { return "hello"; }
//`world()`将不可见.
char const* world() { return "world"; }
//`1()`和`0()`,都可见.
export
{
int one() { return 1; }
int zero() { return 0; }
}
//导出`名字空间`也可行:可看见`hi::english()`和`hi::french()`.
export namespace hi
{
char const* english() { return "Hi!"; }
char const* french() { return "Salut!"; }
}
第三类(4,5,6),导入一个模块单元(分块或头文件等)并重新导出,即如上支持聚集接口中的重导出,如下:
cpp
//`A.cpp`(`"A"`的主`模块接口`单元)
export module A;
export char const* hello() { return "hello"; }
//`B.cpp`(`"B"`的主`模块接口`单元)
export module B;
export import A;
//这种
export char const* world() { return "world"; }
//`main.cpp(`非`模块单元`)
#include <iostream>
import B;
int main()
{
std::cout << hello() << ' ' << world() << '\n';
}
或:
cpp
//`A.cpp("A"`的主`模块接口`单元)
export module A;
import <iostream>;
export import <string_view>;
export void print(std::string_view message)
{
std::cout << message << std::endl;
}
//`main.cpp(`非`模块单元`)
import A;
int main()
{
std::string_view message = "Hello, world!";
print(message);
}
聚集接口,可在此模块内导入并导出的模块,既可在本模块内使用,也可在引入本模块的其它单元中使用.
最后一类是最后两个(7,8),也即如前的模块的权限,全局模块和私有模块,如下:
cpp
//全局模块`A.cpp`(`'A'`的主`模块接口`单元)
module;
//根据`POSIX`标准定义`_POSIX_C_SOURCE`,向标准`头文件`添加功能.
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
export module A;
import <ctime>;
//(这很糟糕,`随机性`很差`)`.改用`C++`<随机`>`.
export double weak_random()
{
std::timespec ts;
std::timespec_get(&ts, TIME_UTC);
//从`<ctime>`.
//根据`POSIX`标准,在`<stdlib.h>`中提供
srand48(ts.tv_nsec);
//`drand48()`返回一个介于0到1间的随机数.
return drand48();
}
//`main.cpp` (非`模块单元`)
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;
//结束`模块接口`单元中可能影响其他翻译单元行为的部分,启动一个私有`模块片段`.
int f()
//无法从`福`导入处取得定义
{
return 42;
}
在c++23中,还支持了模块片段,也就是可随时定义模块分块(如前其实就是使用的模块片段),大家可简单理解为更细粒度的模块分块.
模块的命名,如前最好是物理映射实现模块的描述或声明,比如"abc.test.core.demo",逗号自身无意义.
在命名模块中,必须且只有一个模块声明单元没有指定模块分块,一定要注意.该模块单元叫主模块接口单元.
模块的分块
即模块分块就是以冒号":"为标志的模块切片,冒号前是模块名,后为分块名.即如下定义:
cpp
export module A:B;
//对`'A'`模块的`':B'`分块,声明`模块接口`单元.
可按模块聚集构成新的接口导出它,方式为:
cpp
export(可选) import `模块分块` 属性(可选) ;
再看下模块分块的示例:
cpp
//`A.cpp`
export module A;
//主`模块接口`单元
export import :B;
//导入`"A"`时,可见`Hello()`.
import :C;
//现在仅在`'A`,WorldImpl()`.cpp'`处可见.
//export import :C;
//错误:无法`导出模块`实现单元.
//任何导入`'A'`的`翻译单元`都可看到`World()`.
export char const* World()
{
return WorldImpl();
}
//`AB.cpp`
export module A:B;//`模块分块接口单元`
//任何导入`'A'`的翻译单元,都可看到Hello()`.
export char const* Hello() { return "Hello"; }
//`AC.cpp`
module A:C;//分块模块实现单元
//任何导入`':C'`的`模块单元``"A"`可见`WorldImpl()`.
char const* WorldImpl() { return "World"; }
//`main.cpp`
import A;
import <iostream>;
int main()
{
std::cout << Hello() << ' ' << World() << '\n';
//`WorldImpl();`
//错误:`WorldImpl()`不可见.
}
c++23中的分块片段代码如下:
cpp
//`单个文件`中的`模块分块`
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; }
全局模块和私有模块及模块内成员区别
全局模块中的导出成员跨模块可见;私有模块中的成员则只能本模块内可见;而模块内的普通成员则根据实际定义属性确定,如下:
cpp
export module demo;
//`1`.全局导出:外部可见
export void pub_func();
//`2`.`私有模块`:模块内可见`(C++20)`
module :private;
//私有分块
void local_func();
//`3`.传统方式:静态为本文件可见
static void in_func();
编译处理
在全局模块中,每次修改都会影响模块接口,并自动内联,构成一次编译多次使用(模块的优势);而私有模块修改因其私有导致无法影响模块接口;
模块内的函数则仍按普通C++的编译模块单元情况处理.
模块分块片段中的成员与普通模块中的成员不同主要在于可由模块对分块内的成员更细粒度的导出控制.同时,分块片段,可增量编译,编译速度会更快.
导出控制
几个关键的术语:
聚集模块和子模块
子模块其实就是如前的"a.b.c.d"该形式中,下一层是上一层的子模块.映射物理目录中,子目录中的文件就是父目录中的子模块.
而聚集模块则即多个子模块组成的模块,比如上面的"a","b"等都是聚集模块.
在实际使用时,既可精确引用指定的模块,也可为了方便,一次引入最顶级的聚集模块.类似:
cpp
//`demo.ixx`:`聚集单元`声明
export module demo;
//`重新导出`所有`子模块`,提供统一入口
export import demo.core;
export import demo.math;
export import demo.dbus;
export import demo.control;
//导入`示例`所有功能
//`test.cpp`
import demo;
//导入所有`子模块`
命名模块
命名模块很好理解,就是模块有名,则就可用名字控制
匿名模块
匿名模块就是没有定义模块的名字,匿名模块需要显式声明并认为是全局模块.匿名模块接近于传统的库的应用方式.见代码:
cpp
//`头文件`:`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);
实际开发中,可用导出关键字和模块分块及片段精细化的导出控制和权限处理.命名和匿名的模块方式来更高层的组织控制;
然后子模块实现层级级联效果的显式控制和权限设置.
一般,要在模块的顶级,显式控制对外接口,确保接口的可控性和一致性;而子模块级联的内部成员的关系处理,要尽量做到子模块内部的高内聚.