外观
【06】变量参与 C++ 的编译
约 2063 字大约 7 分钟
教学视频
写在前面
如果你觉得本教程对你有所帮助,欢迎一键三连,你的支持是我不断更新的动力!
1. 前言
在开发中,往往会有以下需求,需要用户或者程序员来选择编译某段代码。例如在C/C++语言中,这种通常的做法是使用预编译命令#define:
#define HAVE_ABC
/* code */
#ifdef HAVE_ABC
class ABC
{
/* code */
};
#endif // HAVE_ABC这种需要手动修改源码,操作上不安全且比较繁琐。如果能在程序编译期间产生
- 某些预编译宏
- 包含宏、常量表达式等内容的头文件
将在安全性、操作的便捷性上会有显著提升。编译器提供了一些指令,可以在编译期间产生预编译宏,例如 GCC 的 -D 选项:
g++ -DXXX main.cpp这样就可以在编译期间产生一个预编译宏 XXX,CMake 则提供了一组功能,可以在 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_NONFREE2.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
在 【05】目标构建 - 普通库目标构建中提到了 target_compile_definitions 命令,这也是目标属性当中 “使用要求” 的一部分。在添加预定义宏的功能上,相比于 add_definitions,target_compile_definitions 则更具有针对性,它能明确的指定是哪个目标在构建的时候引入预编译宏。下面首先给出 OpenCV 中的例子
# 定义于 <opencv-path>/cmake/OpenCVModule.cmake 中 _ocv_create_module 宏中
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 这个预编译宏定义。