24、CMake与第三方库实战:Boost、OpenCV、Eigen、fmt库的查找与集成

说实话,CMake 里最让人头疼的,就是跟第三方库打交道。

你想想看,Boost 那么大一个巨无霸,OpenCV 动不动几百个模块,Eigen 又是个纯头文件库,fmt 倒是轻巧但版本迭代快……每个库的脾气都不一样。我刚开始用 CMake 那会儿,光是让 Boost 跑起来就折腾了一整天。

这一章,我就带你把这四个典型库的查找与集成,一个一个捋清楚。

24.1 第三方库集成的核心思路

不管是什么库,CMake 里集成的套路其实就两条路:

  • Config 模式:库自己提供了 <Package>Config.cmake 文件,直接 find_package 就能用。OpenCV、Eigen、fmt 都走这条路。
  • Find 模式:库没有自带配置文件,需要 CMake 提供一个 Find<Package>.cmake 模块来查找。Boost 就是典型代表。

我个人习惯,优先用 Config 模式。为什么?因为库作者最清楚自己的安装布局,生成的配置文件最可靠。但 Boost 是个例外——它太大了,官方推荐用 FindBoost 模块。

核心原则: 能用 find_package 搞定的,绝不要手动写路径。手动写路径的项目,换台机器就崩。

24.2 Boost:巨无霸库的查找艺术

Boost 是我见过最「重」的第三方库。组件多、依赖复杂、版本差异大。我在项目中遇到过好几次,因为 Boost 版本不对导致编译失败,排查了半天才发现是 boost::filesystemstd::filesystem 冲突了。

24.2.1 基本查找方式

find_package(Boost REQUIRED COMPONENTS
    filesystem
    system
    program_options
)

if(Boost_FOUND)
    target_link_libraries(my_app PRIVATE
        Boost::filesystem
        Boost::system
        Boost::program_options
    )
endif()

这里要注意,Boost 的组件名是大小写敏感的。我见过有人写 FileSystem,结果死活找不到——嗯,正确的写法是 filesystem

24.2.2 处理多版本共存

有时候系统里装了多个 Boost 版本。比如 Ubuntu 20.04 自带 1.71,但你项目需要 1.75。这时候可以指定版本:

find_package(Boost 1.75 EXACT REQUIRED COMPONENTS
    filesystem
    system
)

或者指定路径:

set(BOOST_ROOT "/opt/boost_1_75_0")
find_package(Boost REQUIRED COMPONENTS filesystem system)
我曾经踩过的坑: 在 Windows 上,Boost 的库文件名带编译器和运行时信息,比如 libboost_filesystem-vc142-mt-gd-x64-1_75.lib。如果你用错了运行时库(MT vs MD),链接时会报一堆 unresolved symbol。解决办法是确保 CMAKE_MSVC_RUNTIME_LIBRARY 和 Boost 编译时用的运行时一致。

24.2.3 头文件库组件的处理

Boost 里有些组件是纯头文件的,比如 boost::algorithmboost::lexical_cast。这些不需要链接库文件,只需要添加头文件路径:

find_package(Boost REQUIRED)
target_include_directories(my_app PRIVATE ${Boost_INCLUDE_DIRS})

但说实话,我更推荐用 Boost::headers 这个导入目标:

target_link_libraries(my_app PRIVATE Boost::headers)

这样写更干净,CMake 会自动处理包含目录。

24.3 OpenCV:视觉库的模块化集成

OpenCV 的 CMake 集成,是我见过做得最好的之一。它提供了非常完善的 OpenCVConfig.cmake,几乎开箱即用。

24.3.1 标准查找

find_package(OpenCV REQUIRED COMPONENTS
    core
    imgproc
    highgui
    imgcodecs
)

target_link_libraries(my_app PRIVATE
    ${OpenCV_LIBS}
)

这里有个细节:${OpenCV_LIBS} 是一个列表,包含了所有你请求的模块以及它们的依赖。比如你只请求了 imgproc,它会自动把 core 也带上。

24.3.2 精确控制模块

我个人习惯,不推荐直接用 ${OpenCV_LIBS}。为什么?因为有时候你只想用 coreimgproc,但 ${OpenCV_LIBS} 可能把整个 OpenCV 都链接进来了。更好的做法是:

find_package(OpenCV REQUIRED COMPONENTS core imgproc)

target_link_libraries(my_app PRIVATE
    opencv_core
    opencv_imgproc
)

这样链接更轻量,编译速度也快一些。

24.3.3 处理 OpenCV 版本

OpenCV 3 和 OpenCV 4 的 API 有差异。比如 cv::CascadeClassifier 的加载方式就变了。我建议在 CMake 里显式检查版本:

find_package(OpenCV 4.5 REQUIRED)

if(OpenCV_VERSION VERSION_LESS "4.0")
    message(FATAL_ERROR "需要 OpenCV 4.0 以上版本")
endif()
小技巧: 如果你在 macOS 上用 Homebrew 安装的 OpenCV,它默认安装在 /usr/local/opt/opencv。CMake 通常能自动找到,但如果找不到,可以设置 OpenCV_DIR 环境变量指向 /usr/local/opt/opencv/lib/cmake/opencv4

24.4 Eigen:纯头文件库的优雅集成

Eigen 是个很有意思的库——它全是头文件,没有 .so 或 .lib。这意味着你不需要链接任何库文件,只需要让编译器能找到头文件就行。

24.4.1 查找 Eigen

find_package(Eigen3 REQUIRED)

target_link_libraries(my_app PRIVATE
    Eigen3::Eigen
)

就这么简单。Eigen3::Eigen 是一个 INTERFACE 导入目标,它只添加了包含目录,没有链接任何库。

24.4.2 版本检查

Eigen 的 API 在不同版本间有变化。比如 3.3 版本引入了 Eigen::Index 类型。我建议做版本检查:

find_package(Eigen3 3.3 REQUIRED NO_MODULE)

if(EIGEN3_VERSION_STRING VERSION_LESS "3.3")
    message(WARNING "Eigen 版本较低,某些功能可能不可用")
endif()

注意这里用了 NO_MODULE,强制使用 Config 模式。因为 Eigen 的 FindEigen3.cmake 模块可能版本较旧,不如自带的 Eigen3Config.cmake 准确。

我曾经踩过的坑: 在 Ubuntu 18.04 上,系统自带的 Eigen 是 3.3.4,但 find_package(Eigen3 REQUIRED) 默认走 Find 模式,找到的版本号可能不对。加上 NO_MODULE 后,它会读取 Eigen 头文件里的版本宏,结果就准确了。

24.5 fmt:轻量级格式化库的集成

fmt 库是 C++20 std::format 的前身,轻量、高效、头文件可选。它的 CMake 集成也做得非常现代。

24.5.1 标准查找

find_package(fmt REQUIRED)

target_link_libraries(my_app PRIVATE
    fmt::fmt
)

fmt 提供了两个目标:fmt::fmt(静态库或动态库)和 fmt::fmt-header-only(头文件模式)。

24.5.2 头文件模式

如果你不想链接库文件,可以用头文件模式:

find_package(fmt REQUIRED)

target_link_libraries(my_app PRIVATE
    fmt::fmt-header-only
)

这样编译器会直接包含 fmt 的源码,不需要链接任何 .so 文件。适合小项目或者不想处理动态库依赖的场景。

24.5.3 处理 fmt 版本

fmt 的 API 在 8.0 版本有较大变化,比如 fmt::format 改成了 fmt::format_to。我建议在 CMake 里锁定版本:

find_package(fmt 8.0 EXACT REQUIRED)

if(NOT fmt_FOUND)
    message(FATAL_ERROR "需要 fmt 8.0 版本")
endif()

24.6 知识体系总览

下面这张图,把四个库的集成要点串起来了:

第三方库集成知识体系 find_package Config 模式 vs Find 模式 Boost Find 模式,组件多 OpenCV Config 模式,模块化 Eigen 纯头文件,无链接 fmt 轻量,头文件可选 关键要点 • Boost:用 FindBoost 模块,指定 COMPONENTS,注意版本和运行时库 • OpenCV:用 Config 模式,推荐精确链接模块而非 ${OpenCV_LIBS} • Eigen:用 NO_MODULE 强制 Config 模式,链接 Eigen3::Eigen 目标 • fmt:支持库模式和头文件模式,注意版本兼容性

24.7 实战:一个项目集成四个库

最后,我给你一个完整的例子。假设我们要写一个图像处理程序,用 OpenCV 读图,用 Eigen 做矩阵运算,用 fmt 输出日志,用 Boost 处理命令行参数:

cmake_minimum_required(VERSION 3.16)
project(ImageProcessor VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 查找 Boost
find_package(Boost 1.71 REQUIRED COMPONENTS
    program_options
    filesystem
)

# 查找 OpenCV
find_package(OpenCV 4.5 REQUIRED COMPONENTS
    core
    imgcodecs
    imgproc
)

# 查找 Eigen
find_package(Eigen3 3.3 REQUIRED NO_MODULE)

# 查找 fmt
find_package(fmt 8.0 REQUIRED)

# 添加可执行文件
add_executable(image_processor
    src/main.cpp
    src/processor.cpp
)

# 链接所有库
target_link_libraries(image_processor PRIVATE
    Boost::program_options
    Boost::filesystem
    opencv_core
    opencv_imgcodecs
    opencv_imgproc
    Eigen3::Eigen
    fmt::fmt
)

# 添加编译选项
target_compile_options(image_processor PRIVATE
    -Wall -Wextra -Wpedantic
)

这个 CMakeLists.txt 看起来很长,但每一行都有明确的用途。你想想看,如果没有 CMake,你要手动管理这些库的包含路径、库路径、链接顺序……那得多痛苦。

个人建议: 把常用的 find_package 调用封装到一个 cmake/ 目录下的模块文件里。比如写一个 FindMyLibs.cmake,然后在主 CMakeLists.txt 里只调用一次。这样项目结构更清晰,换机器时也只需要改一个地方。

好了,这一章的内容就到这里。四个库的脾气你都摸清了,以后遇到其他第三方库,思路也是一样的——先看它有没有 Config 文件,没有就找 Find 模块,再不行就自己写查找逻辑。但说实话,99% 的流行库都已经被 CMake 支持了,你只需要知道怎么用就行。


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