文章目录
简介
经典的编译步骤分为三部:
-
预处理: 处理预处理指令, 如
#include
,#define
等 -
编译: 将预处理后的文件编译成汇编代码
-
链接: 将汇编代码链接成可执行文件
来看一个简单的例子:
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}
我们来观察一下预编译:
root@educative:/usercode# g++ -E main.cpp | wc -c
785655
可以看到预编译后的文件有785655个字符, 也就是说预编译后的文件非常大,
这是因为预编译会对头文件做展开, 这些头文件中可能又包含了更多的头文件,
所以预编译后的文件会非常大.
预处理得到的文件是一个编译单元(Translation Unit).
编译过程是将编译单元编译成汇编代码, 生成的文件叫做目标文件,
目标文件是二进制文件.
链接过程是将目标文件链接成可执行文件, 可执行文件是二进制文件.
这个编译过程继承自C, 对单个编译单元来说这样做是没有问题的,
但是当有许多编译单元时, 就会有很多问题出现.
传统编译过程面临的问题
重复的头文件展开
同一个头文件在不同的编译模块中被使用,则会进行多次展开.
比如有多个源文件均包含<vector>
头文件的情形.
编译器宏冲突
C++社区有一个共识就是: 我们应该尽量避免使用宏. 因为宏是一个文本替代,
没有任何C++语义. 考虑下面的例子: 你包含了两个头文件,
这两个头文件中有相同的宏:
// file1.h
#define RED 0XFF0000
在另外一个文件中有如下定义:
// file2.h
#define RED 0XFF0000
那么编译的时候就会有宏定义冲突. 使用了模块之后, 不同的模块是隔离的,
不会有这个问题.
重复包含问题
C++规定在一个编译单元中, 一个符号不能有超过一次的定义. 在程序范围内,
一个函数不能定义超过一次.
为了避免重复定义问题, 头文件一般使用#pragma once
或者 header
guard:
#ifndef _PATH_TO_HEADER_H_
#define _PATH_TO_HEADER_H_
// header file content
#endif
C++ Module(From C++20)
使用C++ Module的好处
-
模块只会被导入一次, 而且开销可以不计
-
模块的导入顺序不重要
-
模块中的重复符号是不允许的
-
模块可以更好地表达代码的逻辑结构, 可以明确指定导出或不导出的名称,
也可以将几个模块捆绑成一个更大的模块, 并将其作为一个逻辑包提供给客户 -
因为有了模块, 就不需要将源代码分为接口和实现部分
模块文件结构
module; // global module fragment
#include <traditional headers>
export module module_name; // module declaration; starts the module purview
import <other modules>;
<non-exported declarations> // names only visible inside the module
export namespace sample {
<exported declarations> // exported names
} // namespace sample
export
命令
有三种方式使用export
:
导出声明
export module name;
export int foo(int fir, int sec);
export void bar();
导出组
export module name;
export {
int foo(int fir, int sec);
void bar();
}
导出命名空间
export module name;
export namespace name {
int foo(int fir, int sec);
void bar();
} // namespace name
CMake对C++模块的支持
根据
CMake官方博客,
从 3.28
版本开始正式支持C++模块. 本文使用的CMakeLists.txt
文件的设置部分如下:
cmake_minimum_required(VERSION 3.28)
project(CppModule CXX)
set(CMAKE_CXX_STANDARD 23)
后续每个例子都会给出其相应的cmake设置.
一个简单的例子
模块文件
export module simple;
export int add(int a, int b) {
return a + b;
}
export module simple;
是一个模块声明, 它声明了一个模块,
模块名为simple
. 在模块中,
我们可以使用export
关键字导出函数,命名空间, 类, 变量等.
这样的实体可被其他模块导入.
程序主文件
在使用的时候, 使用import simple;
导入模块.
import simple;
int main() {
return add(1, 2);
}
cmake设置
add_library(simple_module)
target_sources(simple_module
PUBLIC
FILE_SET CXX_MODULES FILES
simple.ixx
)
add_executable(simple simple_main.cpp)
target_link_libraries(simple simple_module)
稍微复杂一点的例子
模块文件
module;
#include <string>
#include <utility>
export module complex;
export namespace complex {
export class Person {
public:
Person(unsigned age, std::string name) : age_(age), name_(std::move(name)) {}
[[nodiscard]] std::string Name() const { return name_; }
private:
unsigned age_;
std::string name_;
};
} // namespace complex
程序主文件
import complex;
#include <iostream>
int main() {
complex::Person joy(18, "Joy");
std::cout << joy.Name();
return 0;
}
cmake设置
add_library(complex_module)
target_sources(complex_module
PUBLIC
FILE_SET CXX_MODULES FILES
complex.ixx
)
add_executable(complex complex_main.cpp)
target_link_libraries(complex complex_module)
模块接口(interface)声明与实现
当模块变得越来越大时, 你可能需要把模块的接口和实现分开.
这与头文件和源文件的分离类似.
模块接口单元
module;
#include <vector>
export module demo;
export namespace demo {
int sum(const std::vector<int>& v);
int prod(const std::vector<int>& v);
} // namespace demo
模块实现单元
module demo;
#include <numeric>
#include <vector>
namespace demo {
int sum(const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 1, std::plus{});
}
int prod(const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 1, std::multiplies{});
}
} // namespace demo
主程序
import demo;
#include <iostream>
#include <vector>
int main() {
std::vector vec{1, 2, 3, 4, 5};
std::cout << "sum : " << demo::sum(vec) << std::endl;
std::cout << "prod : " << demo::prod(vec) << std::endl;
}
cmake设置如下:
add_library(separate_module)
target_sources(separate_module
PUBLIC
FILE_SET CXX_MODULES FILES
separate_interface.ixx
)
target_sources(separate_module PUBLIC separate_impl.ixx)
add_executable(separate separate_main.cpp)
target_link_libraries(separate separate_module)
子模块
对于大的模块可以将其分割为多个子模块. 然后设置一个主模块文件,
导入并导出子模块.
模块主文件
sort.ixx
module;
export module sort;
export import sort.bubble_sort;
export import sort.insert;
export import sort.quick;
子模块文件
module;
#include <utility>
#include <vector>
export module sort.bubble_sort;
export namespace sort {
void bubble_sort(std::vector<int>& arr) {
if (arr.size() < 2) return;
for (int i = 0; i < arr.size() - 1; i++) {
auto swapped = false;
for (int j = 1; j < arr.size() - i; j++) {
if (arr[j - 1] > arr[j]) {
std::swap(arr[j - 1], arr[j]);
swapped = true;
}
}
if (!swapped) break;
}
}
} // namespace sort
module;
#include <vector>
export module sort.insert;
export namespace sort {
void insertion_sort(std::vector<int>& arr) {
for (int i = 1; i < arr.size(); i++) {
auto k = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > k) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = k;
}
}
} // namespace sort
程序主文件
import sort;
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector v{5, 4, 3, 2, 1};
sort::bubble_sort(v);
std::cout << std::boolalpha << std::is_sorted(v.begin(), v.end(), std::less{})
<< std::endl;
}
cmake设置
add_library(sort_module)
target_sources(sort_module
PUBLIC
FILE_SET CXX_MODULES FILES
sort.ixx
sort_bubble.ixx
sort_quick.ixx
sort_insert.ixx
)
add_executable(sort_module_demo sort_main.cpp)
target_link_libraries(sort_module_demo sort_module)
模块分区
模块分区与子模块类似, 唯一的区别是分区模块不能作为一个独立模块存在.
模块主文件
module;
export module partition;
export import :part1;
export import :part2;
子分区文件
module;
#include <utility>
#include <vector>
export module partition:part1;
export namespace sort {
void bubble_sort(std::vector<int>& arr) {
if (arr.size() < 2) return;
for (int i = 0; i < arr.size() - 1; i++) {
auto swapped = false;
for (int j = 1; j < arr.size() - i; j++) {
if (arr[j - 1] > arr[j]) {
std::swap(arr[j - 1], arr[j]);
swapped = true;
}
}
if (!swapped) break;
}
}
} // namespace sort
module;
#include <vector>
export module partition:part2;
export namespace sort {
void insertion_sort(std::vector<int>& arr) {
for (int i = 1; i < arr.size(); i++) {
auto k = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > k) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = k;
}
}
} // namespace sort
cmake设置
add_library(partition_module)
target_sources(partition_module
PUBLIC
FILE_SET CXX_MODULES FILES
partition.ixx
partition_part1.ixx
partition_part2.ixx
)
add_executable(partition_module_demo partition_main.cpp)
target_link_libraries(partition_module_demo partition_module)
构建环境
本文使用的测试环境:
-
Microsoft Visual Studio 2022 Preview
-
CLion 2024.1 Beta
根据
CMake官方博客,
从 3.28
版本开始正式支持C++模块.
这里给出我这篇博客的CMakeLists.txt
文件:
cmake_minimum_required(VERSION 3.28)
project(CppModule CXX)
set(CMAKE_CXX_STANDARD 23)
add_library(simple_module)
target_sources(simple_module
PUBLIC
FILE_SET CXX_MODULES FILES
simple.ixx
)
add_executable(simple simple_main.cpp)
target_link_libraries(simple simple_module)
add_library(complex_module)
target_sources(complex_module
PUBLIC
FILE_SET CXX_MODULES FILES
complex.ixx
)
add_executable(complex complex_main.cpp)
target_link_libraries(complex complex_module)
add_library(separate_module)
target_sources(separate_module
PUBLIC
FILE_SET CXX_MODULES FILES
separate_interface.ixx
)
target_sources(separate_module PUBLIC separate_impl.ixx)
add_executable(separate separate_main.cpp)
target_link_libraries(separate separate_module)
add_library(sort_module)
target_sources(sort_module
PUBLIC
FILE_SET CXX_MODULES FILES
sort.ixx
sort_bubble.ixx
sort_quick.ixx
sort_insert.ixx
)
add_executable(sort_module_demo sort_main.cpp)
target_link_libraries(sort_module_demo sort_module)
add_library(partition_module)
target_sources(partition_module
PUBLIC
FILE_SET CXX_MODULES FILES
partition.ixx
partition_part1.ixx
partition_part2.ixx
)
add_executable(partition_module_demo partition_main.cpp)
target_link_libraries(partition_module_demo partition_module)
完整的文件将在附件中给出, 如有需要可以下载实验.
总结
-
模块是C++20的新特性, 它可以解决头文件的低效问题.
导入模块的代价非常低, 并且导入顺序不重要.
模块还可以解决名称冲突问题. -
模块由模块接口单元和模块实现单元组成.
必须有一个导出模块声明的模块接口单元, 以及任意多个模块实现单元.
在模块接口中没有导出的名称具有模块链接, 不能在模块外部使用. -
模块可以有头文件, 也可以导入和重新导出其它模块.
-
C++20标准库没有模块化.
使用C++20构建模块化的软件系统是一项具有挑战性的任务. -
为了构建大型软件系统, 模块提供了两种方法: 子模块和模块分区.
与分区不同, 子模块可以独立存在. -
由于头文件单元的存在, 可以用导入语句替换包含语句,
编译器会自动生成一个模块.