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::filesystem 和 std::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)
libboost_filesystem-vc142-mt-gd-x64-1_75.lib。如果你用错了运行时库(MT vs MD),链接时会报一堆 unresolved symbol。解决办法是确保 CMAKE_MSVC_RUNTIME_LIBRARY 和 Boost 编译时用的运行时一致。
24.2.3 头文件库组件的处理
Boost 里有些组件是纯头文件的,比如 boost::algorithm、boost::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}。为什么?因为有时候你只想用 core 和 imgproc,但 ${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()
/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 准确。
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 知识体系总览
下面这张图,把四个库的集成要点串起来了:
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 支持了,你只需要知道怎么用就行。