第 5 步:深入了解 CMake 库概念¶
虽然可执行文件大多是通用的,但库的形式多种多样。仅举几例,就有静态归档库、共享对象库、模块库、对象库、仅含头文件的库,以及描述高级 CMake 属性并可被其他目标继承的库。
在这一步中,您将了解 CMake 可以描述的最常见的一些库类型。这将涵盖 add_library() 在项目内的大多数用法。从依赖项导入的库(或由项目导出以供作为依赖项使用)将在后续步骤中介绍。
背景¶
正如我们在 Step1 中所学到的,add_library() 命令的第一个参数是所要创建的库目标的名称。第二个参数是可选的 <type>,以下值均有效:
STATIC静态库(Static Library):用于链接其他目标时的目标文件归档。
SHARED共享库(Shared Library):一种可由其他目标链接并在运行时加载的动态库。
MODULE模块库(Module Library):一种插件,不能被其他目标链接,但可以使用类似 dlopen 的功能在运行时动态加载。
OBJECT对象库(Object Library):一组尚未归档或链接到库中的目标文件。
INTERFACE接口库(Interface Library):一种库目标,它为依赖项指定使用需求,但不编译源文件,也不会在磁盘上生成库工件。
此外,还有 IMPORTED 库,它们描述了从外部项目或模块导入当前项目的库目标。我们将在后续步骤中简要介绍这些内容。
MODULE 库最常见于插件系统,或作为 Python 或 Javascript 等运行时加载语言的扩展。它们的作用与普通共享库非常相似,只是不能被其他目标直接链接。它们非常相似,因此我们不再深入讨论。
练习 2 - 接口库¶
接口库是指那些仅传达其他目标使用需求的库,它们本身不构建或产生任何工件。因此,接口库的所有属性都必须是接口属性本身,并使用 INTERFACE 作用域关键字来指定。
add_library(MyInterface INTERFACE)
target_compile_definitions(MyInterface INTERFACE MYINTERFACE_COMPILE_DEF)
C++ 开发中最常见的接口库类型是仅包含头文件的库。此类库不构建任何东西,只提供发现其头文件所需的标志。
目标¶
将一个仅含头文件的库添加到教程项目中,并在 Tutorial 可执行文件中使用它。
有用资源¶
要编辑的文件¶
MathFunctions/MathLogger/CMakeLists.txtMathFunctions/CMakeLists.txtMathFunctions/MathFunctions.cxx
开始¶
在之前关于 target_sources(FILE_SET) 的讨论中,我们提到如果文件集的名称与文件集的类型相同,我们可以省略 TYPE 参数。我们还提到,如果我们想将当前源目录作为唯一的基目录,可以省略 BASE_DIRS 参数。
我们准备引入第三个快捷方式:如果头文件打算被安装(例如库的公共头文件),我们只需要包含 FILES 参数即可。
本练习中的 MathLogger 头文件仅由 MathFunctions 实现内部使用。它们将不会被安装。这应该会使 target_sources(FILE_SET) 的调用变得非常简短。
注意
编译器依赖扫描程序会发现这些头文件,以确保正确的增量构建。无论如何,在这种情况下列出头文件是很有用的,因为该列表可用于生成某些 IDE 所依赖的元数据。
您可以开始编辑 Step5 目录。完成 TODO 1 到 TODO 7。
构建并运行¶
预设已更新为使用 mathfunctions::sqrt 而不是 std::sqrt。我们可以像往常一样进行构建和配置。
cmake --preset tutorial
cmake --build build
验证 Tutorial 的输出现在是否使用了日志框架。
解决方案¶
首先,我们添加一个名为 MathLogger 的新 INTERFACE 库。
TODO 1:点击显示/隐藏答案
add_library(MathLogger INTERFACE)
然后我们添加适当的 target_sources() 调用来捕获头文件信息。我们给这个文件集命名为 HEADERS,这样我们就可以省略 TYPE;我们不需要 BASE_DIRS,因为我们将使用当前源目录的默认值;我们也可以省略 FILES 列表,因为我们不打算安装该库。
TODO 2:点击显示/隐藏答案
target_sources(MathLogger
INTERFACE
FILE_SET HEADERS
)
现在我们可以将 MathLogger 库添加到 MathFunctions 的链接库中,并将 MathLogger 文件夹添加到项目中。
TODO 3-4: 点击显示/隐藏答案
target_link_libraries(MathFunctions
PRIVATE
MathLogger
)
add_subdirectory(MathLogger)
最后,我们可以更新 MathFunctions.cxx 以利用新的记录器。
练习 3 - 对象库¶
对象库有几种高级用途,但也存在一些难以在本教程范围内完全列举的棘手细微差别。
add_library(MyObjects OBJECT)
对象库最明显的缺点是对象本身不能被传递链接。如果一个对象库出现在目标的 INTERFACE_LINK_LIBRARIES 中,链接该目标的依赖项将“看不到”这些对象。在这种情况下,对象库的作用类似于 INTERFACE 库。一般情况下,对象库仅适用于通过 target_link_libraries() 进行 PRIVATE 或 PUBLIC 使用。
对象库的一个常见用例是将多个库目标合并为一个归档或共享库对象。即使在单个项目中,库也可能出于多种原因(例如属于组织内不同的团队)而作为不同的目标进行维护。然而,有时希望将它们作为单个面向消费者的二进制文件进行分发。对象库使这成为可能。
目标¶
向 MathFunctions 库添加多个对象库。
有用资源¶
要编辑的文件¶
MathFunctions/CMakeLists.txtMathFunctions/MathFunctions.hTutorial/Tutorial.cxx
入门¶
我们已经提供了 MathFunctions 库的几个扩展(我们可以想象这些扩展来自我们组织内的其他团队)。花点时间查看 MathFunctions/MathExtensions 中提供的目标。然后完成 TODO 8 到 TODO 11。
构建和运行¶
无需重新配置,我们可以像往常一样进行构建。
cmake --build build
验证 Tutorial 的输出现在是否包含验证消息。还要花点时间检查 build/MathFunctions/MathExtensions 下的构建目录。您应该发现,与 MathFunctions 不同,没有任何对象库产生归档文件。
解决方案¶
首先,我们将所有对象库的链接添加到 MathFunctions。它们是 PUBLIC 的,因为我们希望将对象作为其自身构建步骤的一部分添加到 MathFunctions 库中,并且我们希望该库的使用者能够使用这些头文件。
然后我们向项目添加 MathExtensions 子目录。
TODO 8-9: 点击显示/隐藏答案
target_link_libraries(MathFunctions
PRIVATE
MathLogger
PUBLIC
OpAdd
OpMul
OpSub
)
add_subdirectory(MathExtensions)
为了让使用者能够使用这些扩展,我们在 MathFunctions.h 头文件中包含了它们的头文件。
TODO 10: 点击显示/隐藏答案
#include <OpAdd.h>
#include <OpMul.h>
#include <OpSub.h>
最后,我们可以利用 Tutorial 程序中的这些扩展。
TODO 11: 点击显示/隐藏答案
double const checkValue = mathfunctions::OpMul(outputValue, outputValue);
std::cout << std::format("The square of {} is {}\n", outputValue,
checkValue);