0-前置知识及cmake入门
前置知识/背景
cpp文件的编译与链接
假设我们有个代码文件main.cpp
,单个文件可以通过以下指令进行编译和运行:
# 编译
g++ main.cpp -o a.out
# 运行
./a.out
单文件编译虽然很方便,但也有以下缺点:
- 所有的代码都堆在一起,不利于模块化管理
- 单文件代码量很大时,编译时间很长,十分麻烦
因此,提出多文件编译的概念,文件之间通过 符号声明 相互引用:
# 分为m1模块和main主模块
# -c 编译生成临时的对象文件
g++ -c m1.cpp -o m1.o
g++ -c main.cpp -o main.o
# 对他们进行链接,得到最终可执行文件
g++ m1.o main.o -o a.out
这样做解决了单文件编译的缺点,但随着模块的增长,敲的指令也变多了,很麻烦、
Makefile
Makefile
便诞生了,只需一个文件和一个指令即可完成上边的操作:
Makefile:
a.out: m1.o main.o
g++ m1.o main.o -o a.out
m1.o: m1.cpp
g++ -c m1.cpp -o m1.o
main.o: main.cpp
g++ -c main.cpp -o main.o
指令:
make
# 加快速度
make -j
但是也有一些不足:
make
指令在Unix上通用,但Windows不是- 要准确指明每个项目间的依赖关系,规模大的时候(如
hit-oslab
)会很头疼。 make
语法简单,不能做很多判断。- 不同编译器的指令不一样。
库(library)
有时候会碰到多个可执行文件共用某些功能的情况,可以把这些共用的功能做成一个 库 ,方便共享。
库中的函数可以被可执行文件调用,也能被其它库文件调用。库文件分为 静态库文件 和 动态库文件(dll, so) 两种。静态库相当于直接把代码插到可执行文件中,会导致体积变大;动态库会在可执行文件相应位置生成“插桩”函数,执行时会先读取dll文件,将对应功能加载到内存空闲位置,执行到此处会跳转到加载后的地址(插桩在读取dll后会被替换)。
Windows中,先会在可执行文件所在目录查找dll,其次是环境变量%PATH%
;Linux中,先在elf可执行文件的RPATH
中查找,其次是/usrt/lib
等。
CMake入门
为了解决make
的以上问题,跨平台的CMake
便诞生了。上边Makefile
对应的CMakeLists.txt
如下:
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp m1.cpp)
这是CMakeLists.txt
文件的基本格式,其中,第4行的意思是,a.out
为输出的文件,后面的全是输入的文件。
CMake的命令行调用
读取当前目录的CMakeLists.txt
,并在build
文件夹下生成build/Makefile
:
cmake -B build
让make
读取build/Makefile
,并开始构建a.out
:
make -C build
# 也可以选择更跨平台的做法
make --build build
最后便可以执行build/a.out
了。
CMake中的静态库与动态库
可以使用add_library
生成库文件:
# 生成静态库 libtest.a/lib
add_library(test STATIC s1.cpp s2.cpp)
# 生成动态库 libtest.so/dll
add_library(test SHARED s1.cpp s2.cpp)
初学建议用静态库,动态库有坑,但他人提供的库大多数是作为动态库的。
创建库后,要在某个 可执行文件 中使用该库,需要:
# 为 myexec 链接刚刚制作的库 libtest.a
target_link_libraries(myexec PUBLIC test)
CMake中的子模块
在复杂的工程中,需要划分子模块,通常一个库一个目录。例如以下文件结构:
module-1
|- CMakeLists.txt
|- m1.cpp
|- m1.h
CMakeLists.txt
main.cpp
模块的CMakeLists.txt
如下:
add_library(m1lib STATIC m1.cpp)
要想在根目录中使用子模块,CMakeLists.txt
可以这样写:
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
# 使用子模块m1lib
add_subdirectory(m1lib)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC m1lib)
此外,还得修改main.cpp
中m1.h
的路径。如果要避免修改代码,可以通过target_include_directories
指定a.out
的头文件搜索目录:
# line 9
target_include_directories(a.out PUBLIC m1lib)
这样子指定的路径也被视为系统路径(可以用尖括号包裹起来)。
但是,以上做法违反了 不要重复自己(DRY) 原则,即如果还有其他可执行文件要使用m1
模块,还得添加这一行。因此,可以直接将这句话添加到m1
模块的CMakeLists.txt
中:
# line 2
target_include_directories(m1lib PUBLIC .)
PUBLIC
和PRIVATE
决定一个属性要不要在被链接的时候传播。
目标(target)的一些其他选项
target_include_directories(myapp PUBLIC /usr/...)
:添加头文件搜索目录
target_link_libraries(myapp PUBLIC hellolib)
:添加要链接的库
target_add_definitions(myapp PUBLIC MY_MACRO=1)
:添加一个宏定义
target_add_definitions(myapp PUBLIC -DMY_MACRO=1)
:添加一个宏定义
target_compile_options(myapp PUBLIC -fopenmp)
:添加编译器命令行选项
target_sources(myapps hello.cpp other.cpp)
:添加要编译的源文件
引入第三方库
第三方库的引入主要分为三类:纯头文件,作为子模块,系统预安装。
纯头文件库:操作起来最简单,只需把他们的
include
目录或头文件下载下来,然后include_directories(xxxx/include)
即可,例如:- 图形学相关的
stb_image库
,只需添加一个宏定义STB_IMAGE_IMPLEMENTATION
并引入一个头文件即可。 magic_enum
库(实现枚举类的自反射,枚举名以字符串形式输出)glm
库:模仿GLSL语法的数学矢量/矩阵库Tencent/rapidjson
:JSON解析库,无STL内容range-v3
:C++20的range
受到他启发而成,但没他好用fmt
:提供std::format
的替代品,需要宏定义FMT_HEADER_ONLY
.
缺点:函数直接实现在头文件里,没有提前编译,编译时间长。
- 图形学相关的
作为子模块:可以作为CMake子模块引入,即通过
add_subdirectory
fmt
range-v3
glm
abseil-cpp
:补充标准库没有的常用功能backward-cpp
:实现C++的堆栈回溯,便于调试googletest
:谷歌单元测试框架benchmark
:谷歌性能评估框架glfw
:OpenGL窗口和上下文管理libigl
:各种图形学算法大合集
缺点:可能会出现
菱形依赖
,即某个模块被两个模块使用,发生重复定义。系统预安装:可以通过
find_package
命令寻找系统中的包/库:find_package(fmt REQUIRED) target_link_libraries(myexec PUBLIC fmt::fmt)
现代CMake认为一个包可以提供多个库,又称为组件,为了避免冲突,每个包都享有一个独立的名字空间。例如TBB这个包中有tbb,tbbmalloc,tbbmalloc_proxy这三个组件,可以指定要用那几个组件:
find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED) target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)
常用
package
列表如下:fmt::fmt
range-v3::range-v3
TBB::tbb
OpenVDB::openvdb
Boost::iostreams
Eigen3::Eigen
OpenMP::OpenMP_CXX
不同的包之间常常有依赖关系,包管理器的作者会给
find_package
编写脚本(xxxConfig.cmake),可以自动且正确处理依赖项。包的引用格式和文档可以参考FindBLAS.
包管理器
Linux可以用包管理器来安装第三方库(如apt
,pacman
)等,Windows没有自带的包管理器,可以用微软跨平台的vcpkg
包管理器。
使用方法:下载vcpkg
的源码,放到项目根目录,如图:
然后执行以下命令:
cd vcpkg
./bootstrap-vcpkg.bat
./vcpkg intergrate install
./vcpkg install fmt:x64-windows # 安装fmt库(默认最新,无法指定版本)
cd ..
cmake -B build -DCMAKE_TOOL_CHAIN_FILE="%CD%/vcpkg/scripts/buildsystems/vcpkg.cmake"
参考资料
- 小彭老师 - 高性能并行编程与优化