【CMake】大型项目中CMake的使用技巧

在这里插入图片描述

【CMake】大型项目中CMake的使用技巧

1 大型项目的一般架构

在实际程序开发过程中,会经常使用到第三方库,而这些第三方库通常是一些大型的项目,例如计算机视觉库OpenCV,视觉伺服平台visp等。这些库为了满足多人同时开发的功能,在代码逻辑架构、文件架构上都有比较深的讲究。

1.1 文件架构

一般来说,源代码的文件夹会被命名为src,若是 C/C++ 的大型项目,还会存在以 include 为头文件的文件夹。除此之外,一些测试文件均被放在 test 文件夹下,并且在这些均被放在同级目录之下,在该目录下还可能存在 CMakeLists.txt。在项目最高级目录下还会存在 samples 文件夹,很多测试 demo 被放入其中,最后还会给用户提供一个 README.md 描述文件,就拿 OpenCV 的部分文件来举例子:

.
├── cmake
│   └── ...
├── CMakeLists.txt
├── include
│   ├── CMakeLists.txt
│   └── ...
├── modules
│   └── core
│       ├── CMakeLists.txt
│       ├── include
│       │   └── ...
│       ├── src
│       │   └── ...
│       └── test
│           └── ...
├── README.md
└── samples

1.2 CMakeLists 在其中的作用

很显然,每个模块的文件夹下都会存在一个CMakeLists.txt。实际上,最底层的CMakeLists.txt是为了管理该模块功能的编译、测试文件编译,以及提供指定的接口(可以是库,也可以是若干 CMake 变量)给外部。上一级的CMakeLists.txt则可以根据需求,选择需要编译的一个或若干功能模块,并且在某些联合编译的情况下链接需要的库。最高一级CMakeLists.txt一般是对于整个库编译选项的设置,以及对系统、平台、编译器等编译环境信息的兼容处理

2 CMake 变更项目信息技巧

2.1 按需求配置编译选项

2.1.1 使用介绍

往往一个项目会涉及到几个彼此独立的模块,一般情况下,可手动配置所需要的部分,在实现根据选项配置内容时需要用到 option(),例如在CMakeLists.txt中写入以下内容:

option(A "a" OFF)
if (A)
    # ...
endif ()

此时,我们为 CMake 提供了一个选项 A,并且默认情况下为关闭状态(OFF)。若在命令行中输入以下内容(假设CMakeLists.txt在当前目录的上一级):

cmake -D A=ON ..

则可以在 cmake 过程中将 A 选项开启,则可以使 if 语句块中的内容参与编译内容的制作。

2.1.2 案例

现设计一个识别模块 detector ,具有一个抽象的识别器头文件 detector.h,以及两个具体的识别器派生类 A_detectorB_detector,其代码结构如下:

在这里插入图片描述

若用户想在该模块上添加一个测试文件 test.cpp,并且仅用到了 A_detector 识别器,因此需要在编译时选择编译 A_detector 的功能,而不编译 A_detector 的功能。

为此我们需要在该模块的 CMakeLists.txt 中写入:

# ...
if (A)
    aux_source_directory(src/A_detector A_DIRS)
    # 此处用 SHARED 也可换成 STATIC,代码仅做展示
    add_library(A_detector SHARED ${A_DIRS})
    target_include_directories(A_detector PUBLIC include/A_detector)
    target_link_libraries(A_detector xxx)
elseif (B)
    aux_source_directory(src/B_detector B_DIRS)
    add_library(B_detector SHARED ${B_DIRS})
    target_include_directories(B_detector PUBLIC include/B_detector)
    target_link_libraries(B_detector xxx)
endif ()
# ...

并且在命令行中输入以下内容,即可实现仅对 A_detetcor 的使用:

mkdir build

cd build

cmake -D A=ON ..

2.2 按需求配置宏与常量

若需要在代码中根据编译选项来实时添加某些宏定义或者变量,可使用 configure_fileadd_definitions

2.2.1 configure_file

基本用法:

configure_file(xxx.h.in xxx.h)

例如先构建一个项目,其文件结构如下:

在这里插入图片描述

在使用 configure_file 之前需要创建一个 xxx.h.in 文件,用于生成 xxx.h ,其中 xxx.h.in 文件的编写与普通C++文件极为相似,例如:

#cmakedefine CONFIG_DIR "@CONFIG_DIR@"
#define CONFIG_DIR_1 "@CONFIG_DIR@"
constexpr auto CONFIG_DIR_2 = "@CONFIG_DIR@";
#cmakedefine CONFIG_DIR_3 "@CONFIG_DIR_2@"
#define CONFIG_DIR_4 "@CONFIG_DIR_3@"

CMakeLists.txt 中写入:

set(CONFIG_DIR /usr/local/include/opencv4)

configure_file(
    config.h.in
    ${CMAKE_SOURCE_DIR}/config.h
)

在经过 cmake 后,会在 CMakeLists.txt 当前路径下生成 config.h 文件,其内容如下

#define CONFIG_DIR "/usr/local/include/opencv4"
#define CONFIG_DIR_1 "/usr/local/include/opencv4"
constexpr auto CONFIG_DIR_2 = "/usr/local/include/opencv4";
/* #undef CONFIG_DIR_3 */
#define CONFIG_DIR_4 ""

可以看到,第 4 行和第 5 行的区别就在于使用的是 #cmakedefine 还是 #define

除此之外,还可以通过 option() 选项来制作条件编译的宏,例如:在 config.h.in 中写入:

#cmakedefine A
#cmakedefine B

CMakeLists.txt 中写入:

option(A "a" ON)
option(B "b" OFF)
configure_file(
    config.h.in
    ${CMAKE_SOURCE_DIR}/config.h
)

生成的 config.h 文件内容如下:

#define A
/* #undef B */
2.2.2 add_definitions

有时候单纯是为了得到条件编译的标签,例如 #define A 这种,使用 configure_file 需要单独维护一个 .in 文件,因此不够方便。而使用 add_definitions 可以很好的解决这一点问题。还是本文【2.2.1】中的示例文件架构,但此时不再需要任何的 .in 文件,仅需在 cmake 时使用 add_definitions 即可。例如,在 CMakeLists.txt 中写入:

add_definitions(-DA)

在 main.cpp 中写入

#include <iostream>

using namespace std;

#ifdef A
    inline void foo() { cout << "this is A" <<endl; }
#endif // !A
#ifdef B
    inline void foo() { cout << "this is B" <<endl; }
#endif // !A

int main(int argc, char *argv[])
{
    foo();
    return 0;
}

经过cmake、编译后,运行结果如下

this is A

注意事项:
在使用add_definitions语句时,其生成的对象(例如上述例子中的A)具有作用域的属性。即子模块(由add_subdirectory语句生成)中使用add_definitions生成的对象无法在父模块中使用,也无法跨模块使用(子模块除外)。

2.3 跨文件链接

在大型项目中,多人合作开发是不可避免的,假如此时两个人设计了彼此独立的两个库AB,文件结构如下:

在这里插入图片描述

A 库只有若干头文件,B 库有若干头文件、cpp源文件,因此二者在 CMakeLists.txt 的书写上略有差别,首先看 B 库,参考cmake代码如下:

aux_source_directory(src B_DIRS)
add_library(B SHARED ${B_DIRS})
target_include_directories(B PUBLIC include)

可以利用源文件直接构建一个库B,并且将这些头文件绑定在目标(B库)上,因此,本项目在使用时只需链接该库B即可。

  • 不过某些大型项目在编译安装时会将这些头文件(.h)与库文件(Linux下通常是.so,Windows下通常是.dll)彼此分开,并且安装到本地的默认路径下(Linux下一般是/usr/local/xxx,Windows下一般是C:\Program Files)为此需要提供一个能直接获取头文件以及库文件的参数集合,例如计算机视觉库 OpenCV 提供的参数集合是S{OpenCV_INCLUDE_DIRS}${OpenCV_LIBS}

话说回来,如果有的库仅在本项目中使用,那如何构建出只有头文件的参数集合供本项目的其他库使用呢?本案例中的 A 库可以回答这一点。

本案例中由 main.cpp 生成的可执行程序会用到 A 库的内容,但 CMake 语法生成库必须要有源文件(.cpp),因此不能通过 add_library() 语法来生成能被该可执行文件链接的库。不过,可以提供一个长期存在的参数集保存该头文件,并且在生成可执行文件之后包含该参数集即可。A 库中 CMakeLists.txt 参考代码如下:

set(A_INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/include
                   CACHE PATH "A")

其中,CACHE PATH "A" 一句是将集合 A_INCLUDE_DIRS 存储到文件 CMakeCache.txt 中,因此不加这部分内容的话 A_INCLUDE_DIRS 只能被本 CMakeLists.txt 访问,因此在 cmake 中也存在局部变量的说法。

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Cccolt_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值