华南虎视觉组主页华南虎视觉组主页
首页
项目
通用
教程
Gitea
首页
项目
通用
教程
Gitea
  • 现代 CMake 教程

    • 【01】安装与基本介绍
    • 【02】CMake 的文件分布
    • 【03】变量的设置与引用
    • 【04】运算符与条件、循环语句
    • 【05】目标构建
    • 【06】变量参与 C++ 程序的编译
      • 1. 前言
      • 2. 使用模板文件生成头文件
        • 2.1 配置 *.in 模板文件
        • 2.2 configure_file
        • 2.3 与 option 配合
        • 2.4 使用场景
      • 3. CMake 添加预定义变量
        • 3.1 add_definitions
        • 3.2 target_compile_definitions
    • 【07】宏与函数
    • 【08】find_package 详解
    • 【09】生成器表达式
    • 【10】项目的导出与安装
    • 【11】项目实战
    • 【12】CTest
    • 【13】CPack

【06】变量参与 C++ 程序的编译

1. 前言

在开发中,往往会有以下需求,需要用户或者程序员来选择编译某段代码。例如在C/C++语言中,这种通常的做法是使用预编译命令#define:

#define HAVE_ABC
/* code */
#ifdef HAVE_ABC
class ABC
{
    /* code */
};
#endif // HAVE_ABC

这种需要手动修改源码,操作上不安全且比较繁琐。如果能在 CMake 程序的解析期间能够产生

  • 某些预编译宏
  • 包含宏、常量表达式等内容的头文件

将在安全性、操作的便捷性上会有显著提升。

2. 使用模板文件生成头文件

一个最直接的方法,则是生成包含一系列宏定义的头文件,再让源文件去包含这些头文件,达到让用户自主配置的功能。

2.1 配置 *.in 模板文件

在【01】安装与基本介绍中我们曾介绍过 *.in 文件为一个模板文件,CMake 解析期间能够利用这一模板文件生成实际的文件。我们看到 <opencv-path>/cmake/templates/ 文件夹下,包含了一个 cvconfig.h.in 文件,部分内容如下:

/* code */
/* OpenCV compiled as static or dynamic libs */
#cmakedefine BUILD_SHARED_LIBS

/* OpenCV intrinsics optimized code */
#cmakedefine CV_ENABLE_INTRINSICS

/* OpenCV additional optimized code */
#cmakedefine CV_DISABLE_OPTIMIZATION

/* Compile for 'real' NVIDIA GPU architectures */
#define CUDA_ARCH_BIN "${OPENCV_CUDA_ARCH_BIN}"
/* code */

如果从源码编译过 OpenCV,我们则可以在<opencv-path>/build/文件夹下,找到一个叫做cvconfig.h的文件,部分内容如下:

/* code */
/* OpenCV compiled as static or dynamic libs */
#define BUILD_SHARED_LIBS

/* OpenCV intrinsics optimized code */
#define CV_ENABLE_INTRINSICS

/* OpenCV additional optimized code */
/* #undef CV_DISABLE_OPTIMIZATION */

/* Compile for 'real' NVIDIA GPU architectures */
#define CUDA_ARCH_BIN ""
/* code */

可以看到这个cvconfig.h.in文件的大部分语法与cvconfig.h一致,但其中涉及到一些 CMake 变量的地方,这些内容在cvconfig.h中发生了转化。

经过寻找,可以看到:在<opencv-path/cmake/文件夹下的OpenCVGenHeaders.cmake文件的第一行包含这么两句内容:

# platform-specific config file
configure_file("${OpenCV_SOURCE_DIR}/cmake/templates/cvconfig.h.in" "${OPENCV_CONFIG_FILE_INCLUDE_DIR}/cvconfig.h")
configure_file("${OpenCV_SOURCE_DIR}/cmake/templates/cvconfig.h.in" "${OPENCV_CONFIG_FILE_INCLUDE_DIR}/opencv2/cvconfig.h")

我们可以推测,cvconfig.h就是通过cvconfig.h.in文件而生成的,其中用于转化的语句就是configure_file。

2.2 configure_file

基本用法:

configure_file(aaa.h.in bbb.h) # 使用 aaa.h.in 为模板生成 bbb.h

在使用configure_file之前需要创建一个aaa.h.in文件,用于生成bbb.h,其中aaa.h.in文件与待生成的bbb.h文件基本一致,即与普通的 C++ 文件基本一致,只是在需要替换的地方使用 CMake 的相关标志,例如:

#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@"

其中#cmakedefine就表示会在 CMake 执行配置时期,替换为预定义的 CMake 变量或用户定义的变量。此外,使用@xxx@的内容也是模板,也会在 CMake 执行配置时期进行文本替换。

在 CMakeLists.txt 中写入:

set(CONFIG_DIR "aa/bb/cc")
configure_file(
  config.h.in
  ${CMAKE_SOURCE_DIR}/config.h
  @ONLY # 此项可选,@ONLY 表示仅将 *.in 文件中的 @xxx@ 做替换,而 ${xxx} 不做替换
)

可以看到 CMakeLists.txt 只对CONFIG_DIR进行了定义。而第 4 行和第 5 行提到的CONFIG_DIR_2和CONFIG_DIR_3均未被定义。

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

#define CONFIG_DIR "aa/bb/cc"
#define CONFIG_DIR_1 "aa/bb/cc"
constexpr auto CONFIG_DIR_2 = "aa/bb/cc";
/* #undef CONFIG_DIR_3 */
#define CONFIG_DIR_4 ""

2.3 与 option 配合

除此之外,还可以通过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 */

例如,在<opencv-path>/CMakeLists.txt文件下,有:

OCV_OPTION(OPENCV_ENABLE_NONFREE "Enable non-free algorithms" OFF)

在<opencv-path>/cmake/templates/文件夹中的opencv_modules.hpp.in有:

#cmakedefine OPENCV_ENABLE_NONFREE

opencv-nonfree

如果我们通过cmake-gui将OPENCV_ENABLE_NONFREE修改为ON,那么可以确定,会生成:

#define OPENCV_ENABLE_NONFREE

2.4 使用场景

在一系列宏定义变量,或者的时候,使用 configure_file 是很方便的,但如果只有一个或几个的宏定义变量,那么使用 configure_file 来维护一个 *.in 文件将显得十分冗杂。

3. CMake 添加预定义变量

正如上述使用场景所说,单个或少量的宏定义,使用 configure_file 并不方便,下面提供几个解决此问题的方法。

3.1 add_definitions

3.1.1 用法

add_definitions(-DXXX)

在main.cpp中写入

#include <iostream>
using namespace std;
#ifdef XXX
    inline void foo() { cout << "this is XXX" <<endl; }
#endif // XXX
#ifdef YYY
    inline void foo() { cout << "this is YYY" <<endl; }
#endif // YYY
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

编译后,运行结果如下

this is XXX

需要注意的是,add_definitions 生效的文件为当前 CMakeLists.txt 文件以及子目录的 CMakeLists.txt 文件,可类比变量的作用域。并且,对于其中一个 CMakeLists.txt 文件,即使写成

add_executable(demo main.cpp)
add_definitions(-DXXX)

即,在创建可执行程序这个二进制目标之后再书写 add_definitions ,该预编译宏定义,因为该命令作用域,可参考【09】生成器表达式。我们暂时可以这样理解

  • 这一操作等同于使用 g++ main.cpp -DXXX 的命令行进行编译
  • 只要构建出的目标(上面的例子是 demo)所在的作用域中具有预编译宏定义 XXX,那么该宏定义都能生效(即全局生效)。

3.1.2 与 option 配合

实际上,对于全局生效的 add_definitions 我们更多的是将其与 option 命令结合起来使用,参考下面的代码:

# 定义于 <opencv-path>/CMakeLists.txt 中的内容
OCV_OPTION(ENABLE_IMPL_COLLECTION "xxx" OFF )
# code
if(ENABLE_IMPL_COLLECTION)
  add_definitions(-DCV_COLLECT_IMPL_DATA)
endif()

我们只需在终端中输入以下内容,即可实现该编译选项的开启,并添加对应的宏

cmake -D ENABLE_IMPL_COLLECTION=ON ..

3.1.3 注意事项

由于上文中提到 add_definitions 的生效机制与 CMake 中变量的作用域基本一致,因此在一个 CMakeLists.txt 中如果创建多个目标,那么这些目标均能享有这个作用域下的 add_definitions 所带来的效果使用 add_definitions 构建的目标。具体的说,就是在

  • 当前的 CMakeLists.txt 文件

  • 子目录(使用 add_subdirectory 所添加的模块)的 CMakeLists.txt 文件中

生效,例如以下的布局。

project: CMakeLists
    │
    └── A: CMakeLists
         │
         ├── B: CMakeLists
         │    │
         │    └── D: CMakeLists
         │
         └── C: CMakeLists

例如,在A目录中使用add_definitions定义的内容,在A、B、C、D目录中均生效,而B使用add_definitions定义的内容仅在B、D目录中生效

3.2 target_compile_definitions

相比于 add_definitions,target_compile_definitions 更具有针对性,它能明确的指定是哪个目标在构建的时候引入预编译宏。下面首先给出 OpenCV 中的例子

target_compile_definitions(${the_module} PRIVATE CVAPI_EXPORTS)

在 <opencv-path>/modules/core/src 文件夹下的 system.cpp 这一源文件,有引用到 CVAPI_EXPORTS 的地方:

#if defined CVAPI_EXPORTS && defined _WIN32 && !defined WINCE
// ...
#endif

它为指定的模块添加预编译宏,使得该宏不会被暴露给非指定的目标,因此会很安全。并且,一般设置为 PRIVATE 的预编译宏不能被其余包含该目标的其他目标所共享,这与 CMake 目标构建时接触到的 target_include_directories 以及 target_link_libraries 的用法类似,也就是说,以下代码

target_compile_definitions(
  my_lib
  PUBLIC xxx
)

可以实现其他目标在链接 my_lib 这个目标的时候同样享有 xxx 这个预编译宏定义。

Prev
【05】目标构建
Next
【07】宏与函数