12、链接库文件:target_link_libraries的使用,PUBLIC/PRIVATE/INTERFACE关键字
这一讲,我们来聊聊 CMake 里最常用的命令之一——target_link_libraries。说实话,我刚接触 CMake 那会儿,觉得这命令不就是把库链接上嘛,有啥好讲的?直到我在一个中型项目里被依赖传递问题折磨了两天……嗯,从那以后我才真正搞明白 PUBLIC、PRIVATE、INTERFACE 这三个关键字到底在干什么。
先看一个最简单的例子
假设我们有一个可执行文件 myapp,它依赖一个静态库 mylib。最直接的写法是:
add_executable(myapp main.cpp)
add_library(mylib STATIC mylib.cpp)
target_link_libraries(myapp mylib)
这样写,myapp 就能调用 mylib 里的函数了。但问题来了——如果 mylib 本身又依赖了别的库呢?比如 mylib 内部用了 spdlog 做日志。这时候,myapp 需不需要知道 spdlog 的存在?
答案取决于 mylib 的头文件里有没有暴露 spdlog 的类型。这就是 PUBLIC、PRIVATE、INTERFACE 要解决的问题。
三个关键字的本质
我个人习惯把这三个关键字理解成「可见性控制」。它们控制的是:一个目标的依赖信息,要不要传递给链接它的其他目标。
| 关键字 | 对自身的影响 | 对下游的影响 | 典型场景 |
|---|---|---|---|
| PRIVATE | 链接 + 包含路径 | 不传递 | 内部实现细节,不暴露给使用者 |
| PUBLIC | 链接 + 包含路径 | 传递 | 接口中暴露了依赖的类型/宏 |
| INTERFACE | 不链接 | 传递 | 纯头文件库,或只传递编译选项 |
说白了:PRIVATE 是「我自己用,不告诉别人」;PUBLIC 是「我用,你也用」;INTERFACE 是「我不需要,但你需要」。
实战场景一:PRIVATE 的正确用法
我在项目中遇到过这样一个场景:我们写了一个网络库 netlib,内部用 openssl 做加密。但 netlib 的对外头文件里,没有任何 openssl 相关的类型或函数。这时候,openssl 就应该用 PRIVATE 链接:
add_library(netlib SHARED netlib.cpp)
target_link_libraries(netlib PRIVATE openssl)
add_executable(client client.cpp)
target_link_libraries(client netlib)
// client 不需要知道 openssl 的存在
这样做的好处很明显:client 的开发者根本不用关心 netlib 内部用了什么加密库。如果以后我们把 openssl 换成 mbedtls,只需要改 netlib 的 CMakeLists.txt,client 完全不受影响。
实战场景二:PUBLIC 的典型用法
反过来,如果你的库在头文件里直接引用了另一个库的类型,那就必须用 PUBLIC。举个例子:
// mylib.h
#include <Eigen/Dense>
class MyLib {
Eigen::MatrixXd compute();
};
// CMakeLists.txt
add_library(mylib mylib.cpp)
target_link_libraries(mylib PUBLIC Eigen3::Eigen)
这里 Eigen 是 PUBLIC 的,因为任何包含 mylib.h 的代码,都需要知道 Eigen::MatrixXd 是什么。如果你用了 PRIVATE,编译 client 时就会报「找不到 Eigen/Dense」的错误。
实战场景三:INTERFACE 的妙用
INTERFACE 最典型的应用场景是纯头文件库。比如我们写了一个模板库 mymath,只有 .hpp 文件,没有 .cpp 文件:
add_library(mymath INTERFACE)
target_include_directories(mymath INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(mymath INTERFACE Eigen3::Eigen)
add_executable(test test.cpp)
target_link_libraries(test mymath)
注意这里 mymath 本身不需要编译,所以用 INTERFACE 表示「我只传递依赖,自己不链接任何东西」。这样 test 就能自动获得 Eigen 的包含路径和链接信息。
另一个常见用法是:你想给一组目标统一添加编译选项。比如:
add_library(compile_options INTERFACE)
target_compile_options(compile_options INTERFACE -Wall -Wextra)
target_link_libraries(myapp PRIVATE compile_options)
target_link_libraries(mylib PUBLIC compile_options)
这样 myapp 和 mylib 以及 mylib 的使用者,都会加上 -Wall -Wextra。
知识体系图
下面这张图帮你理清三个关键字的关系:
几个容易忽略的细节
嗯,这里要注意几个点:
- 顺序问题:
target_link_libraries必须在add_library或add_executable之后调用。这是 CMake 的硬性要求,没得商量。 - 重复链接:CMake 会自动去重,不用担心同一个库被链接多次。但如果你手动写了两次,它也不会报错,只是忽略第二次。
- 接口库的链接:
INTERFACE库可以链接另一个INTERFACE库,形成传递链。这在大型项目中很常见。
总结一下
target_link_libraries 的三个关键字,说白了就是控制依赖的「传播范围」。你想想看,如果一个库的内部实现细节被暴露给了所有使用者,那以后改起来得多痛苦?反过来,如果该暴露的没暴露,使用者又没法编译。
我个人习惯在写每个 target_link_libraries 时,都问自己三个问题:
- 这个依赖是编译时必需,还是运行时必需?
- 这个依赖的类型/宏,会不会出现在我的头文件里?
- 如果以后换掉这个依赖,我希望影响范围有多大?
想清楚这三点,你就能用好这三个关键字了。
公众号:蓝海资料掘金营,微信deep3321