【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
如果我们通过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
这个预编译宏定义。