12、链接库文件:target_link_libraries的使用,PUBLIC/PRIVATE/INTERFACE关键字

这一讲,我们来聊聊 CMake 里最常用的命令之一——target_link_libraries。说实话,我刚接触 CMake 那会儿,觉得这命令不就是把库链接上嘛,有啥好讲的?直到我在一个中型项目里被依赖传递问题折磨了两天……嗯,从那以后我才真正搞明白 PUBLICPRIVATEINTERFACE 这三个关键字到底在干什么。

先看一个最简单的例子

假设我们有一个可执行文件 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 的类型。这就是 PUBLICPRIVATEINTERFACE 要解决的问题。

三个关键字的本质

我个人习惯把这三个关键字理解成「可见性控制」。它们控制的是:一个目标的依赖信息,要不要传递给链接它的其他目标

关键字 对自身的影响 对下游的影响 典型场景
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)

这里 EigenPUBLIC 的,因为任何包含 mylib.h 的代码,都需要知道 Eigen::MatrixXd 是什么。如果你用了 PRIVATE,编译 client 时就会报「找不到 Eigen/Dense」的错误。

我曾经踩过的坑:有一次我把一个 PUBLIC 依赖写成了 PRIVATE,结果 CI 上编译通过,但同事拉代码后编译失败。查了半天才发现,是因为他的编译器版本不同,头文件搜索路径顺序不一样。从那以后,我每次写 target_link_libraries 都会多花 10 秒想清楚:这个依赖到底要不要暴露出去?

实战场景三: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)

这样 myappmylib 以及 mylib 的使用者,都会加上 -Wall -Wextra

知识体系图

下面这张图帮你理清三个关键字的关系:

target_link_libraries 关键字传递逻辑 PRIVATE PUBLIC INTERFACE 仅自身链接 不传递给下游 自身链接 并传递给下游 自身不链接 仅传递给下游 下游目标(链接当前库的目标) 不传递 传递 传递 总结:PRIVATE 藏起来,PUBLIC 传下去,INTERFACE 只传不藏

几个容易忽略的细节

嗯,这里要注意几个点:

  • 顺序问题target_link_libraries 必须在 add_libraryadd_executable 之后调用。这是 CMake 的硬性要求,没得商量。
  • 重复链接:CMake 会自动去重,不用担心同一个库被链接多次。但如果你手动写了两次,它也不会报错,只是忽略第二次。
  • 接口库的链接INTERFACE 库可以链接另一个 INTERFACE 库,形成传递链。这在大型项目中很常见。
我的一个小技巧:当你拿不准该用 PUBLIC 还是 PRIVATE 时,先全部写成 PRIVATE。然后编译你的测试程序,如果报找不到头文件或链接错误,再改成 PUBLIC。这样能最大程度减少不必要的依赖传递。

总结一下

target_link_libraries 的三个关键字,说白了就是控制依赖的「传播范围」。你想想看,如果一个库的内部实现细节被暴露给了所有使用者,那以后改起来得多痛苦?反过来,如果该暴露的没暴露,使用者又没法编译。

我个人习惯在写每个 target_link_libraries 时,都问自己三个问题:

  1. 这个依赖是编译时必需,还是运行时必需?
  2. 这个依赖的类型/宏,会不会出现在我的头文件里?
  3. 如果以后换掉这个依赖,我希望影响范围有多大?

想清楚这三点,你就能用好这三个关键字了。


公众号:蓝海资料掘金营,微信deep3321