前置知识/背景

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.cppm1.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 .)

PUBLICPRIVATE决定一个属性要不要在被链接的时候传播。

目标(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可以用包管理器来安装第三方库(如aptpacman)等,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"

参考资料

  • 小彭老师 - 高性能并行编程与优化

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

阅读全文 »
0%