第五步:深入 CMake 库概念¶
虽然可执行文件大多是千篇一律的,但库却有很多不同的形式。有静态库、共享库、模块库、对象库、仅头文件库,以及描述要由其他目标继承的高级 CMake 属性的库,这仅仅是其中的一小部分。
在这一步中,您将了解 CMake 可以描述的一些最常见的库类型。这将涵盖 add_library() 命令在项目中的大多数用途。从依赖项导入(或项目作为依赖项导出)的库将在后续步骤中介绍。
背景¶
正如我们在 Step1 中所学到的,add_library() 命令接受要创建的库目标的名称作为其第一个参数。第二个参数是一个可选的 <type>,其有效值为:
此外,还有 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);