头文件包含一直是C/C++的传统,它使代码声明与实现分离,但它有一个非常大的问题就是会被重复编译,拖累编译速度。
通常一个标准头文件iostream
展开后可能达几十万甚至上百万行。笔者使用下面的示例进行测试,新建一个main.cc
,内容如下:
#include <iostream>
int main(int argc, char* argv[])
{
return 0;
}
然后分别使用g++和clang++来测试行数:
g++ -E main.cc | wc -c
1003912
clang++ -E main.cc | wc -c
999191
随着C++ 20 Module的出现以及各编译器对其的逐步实现,C++也能进行模块化编程,提高编译速度了。
由于历史原因,现有以头文件形式组织的C++代码,不会在短时间内消失,这种情况将持续相当长的时间,也许是几年,十几年,甚至几十年或者更长,所以目前的C++依旧可以在Module中包含头文件,让之与模块(Module)共存,方便使用现有代码。
目前主流的C++编译器有GCC、Clang和MSVC,各个编译器实现的进展不一,使用的命令行参数也不一样,为了简单起见,笔者先以GCC编译器的命令行和Makefile为例来介绍模块的基本写法及编译,再介绍Clang和MVVC使用CMake来编译项目。
一、模块基础
1. 定义模块
libA.cpp:
export module libA;
export int plus(int x, int y)
{
return x + y;
}
2. 使用模块
main.cpp
import libA;
int main(int argc, char *argv[])
{
plus(1, 2);
return 0;
}
3. 编译:
g++ -std=c++20 -fmodules-ts -c libA.cpp
g++ -std=c++20 main.cpp -c main
g++ -std=c++20 libA.o main.o -o main
需要先编译libA,再编译main
二、声明与定义分离
1. 模块分区
前面的libA
模块代码全部在一个文件中,当代码比较多的时候,清晰度就会下降,可以使用声明与定义分离的形式:
api.cpp:
export module libA;
export
{
int plus(int x, int y);
}
plus.cpp
module libA;
int plus(int x, int y)
{
return x + y;
}
编译:
g++ -std=c++20 -fmodules-ts -c api.cpp
g++ -std=c++20 -fmodules-ts -c plus.cpp
g++ -std=c++20 main.cpp -c main
g++ -std=c++20 api.o plus.o main.o -o main
注意编译顺序,一定是要先编译模块接口(声明)文件api.cc,再编译模块定义(实现)文件plus.cc,否则会报错:
libA: error: failed to read compiled module: No such file or directory
libA: note: compiled module file is 'gcm.cache/libA.gcm'
libA: note: imports must be built before being imported
libA: fatal error: returning to the gate for a mechanical issue
compilation terminated.
三、Module Partition(模块分区)
当一个模块功能比较多时,C++支持将模块进行分区,每个文件只是模块中的一部分,这样可以将模块的接口与实现进一步分离。
libA/minus.cpp
export module libA:minus;
export int minus(int x, int y)
{
return x - y;
}
libA/plus.cpp
export module libA:plus;
export int plus(int x, int y)
{
return x + y;
}
libA/test.cpp
export module libA:test;
import <iostream>;
#include <string.h>
export class CTest
{
public:
CTest()
{
printf("CTest()\n");
}
void foo()
{
printf("foo\n");
}
};
libA/z.cpp
export module libA;
export import :plus;
export import :minus;
export import :test;
接口文件中引用分区模块时可以省略主模块名,也可以指定主模块名:
export import :plus;
与export import libA:plus;
都可以。
编译:
g++ -std=c++20 -fmodules-ts -xc++-system-header iostream
g++ -std=c++20 -fmodules-ts -c libA/minus.cpp
g++ -std=c++20 -fmodules-ts -c libA/plus.cpp
g++ -std=c++20 -fmodules-ts -c libA/test.cpp
g++ -std=c++20 -fmodules-ts -c libA/z.cpp
g++ -std=c++20 main.cpp -c main
g++ -std=c++20 libA/minus.o libA/plus.o libA/test.o libA/z.o main.o -o main
这里同样需要注意编译顺序,先编译系统级头文件,再编译自定义模块。
GCC可以使用import <iostream>;
来引用标准库头文件,但是需要先手动编译,第一句即是,引用得的标准库头文件越多,自己手动编译得也越多。注意:有些标准库头文件目前还不能使用import来引用,将头文件编译为模块时会报错。为了最大程度上与其它编译器兼容,目前不建议使用import来引用标准库头文件
自定义模块必须先编译各分区的实现(minus.cpp、plus.cpp、test.cpp),最后编译模块的接口(z.cpp)。
当文件比较多的时候,使用命令行直接一个个文件编译比较慢,也容易出错,所以可以改用Makefile来编译,编写Makefile如下:
CXX := g++
CXXFLAGS := -std=c++20 -fmodules-ts -gdwarf-4
Target := main
SOURCE := $(wildcard libA/*.cpp)
SOURCE += $(wildcard *.cpp)
OBJS := $(addsuffix .o, $(SOURCE))
all: $(Target)
$(Target): std $(OBJS)
$(CXX) $(CXXFLAGS) $(OBJS) -o $(Target)
std:
$(CXX) $(CXXFLAGS) -xc++-system-header iostream
$(CXX) $(CXXFLAGS) -xc++-system-header cmath
%.cpp.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm $(OBJS) gcm.cache $(Target) -rf
此时只需要执行make
即可编译项目,make clean
清除生成的文件。为了让Makefile遵循前面的编译顺序,笔者在文件命名时即按字母顺序进行了相应的排序,所以$(wildcard libA/*.cpp)
得到的文件顺序是符合要求的。
为了让lldb
也可以调试生成的程序,添加了-gdwarf-4
选项。
当源文件有修改,编译时可能会出现CRC不匹配的错误,如下:
需要执行make clean
再make
即可。
2. 子模块
子模块与分区模块非常像,只是子模块使用冒号:
分隔,而子模块使用点号.
分隔。
libA/test.cpp
export module libA.test;
import <iostream>;
#include <string.h>
export class CTest
{
public:
CTest()
{
printf("CTest()\n");
}
void foo()
{
printf("foo\n");
}
};
libA/z.cpp
export module libA;
export import libA:plus;
export import :minus;
export import libA.test;
三、Clang与MSVC使用CMake来编译项目
在顶层目录创建CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(main)
set(CMAKE_CXX_STANDARD 20)
add_subdirectory(libA)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE libA)
在libA目录创建CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(libA)
set(CMAKE_CXX_STANDARD 20)
aux_source_directory(. SOURCE)
#这里需要去掉路径中的./
string(REPLACE "./" "" SOURCE "${SOURCE}")
add_library(${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES FILES
${SOURCE}
)
注意:cmake_minimum_required(VERSION 3.28)
一定要是3.28及以上,且需要去掉模块路径中的./
。