第5章 CMake编译C++

23 阅读4分钟

5.2 编译基础

5.2.1 编译工作

编译器必须执行以下步骤来创建一个目标文件

1.预处理 2.语言分析 3.汇编 4.优化 5.生成二进制文件

预处理:高级的查找和替换。跨编译单元共享声明。是实际编译前的一步。 语言分析:语法分析,有意义的标记,验证是否符合语言规则。语义分析,类型的正确性检查等等。 汇编:根据平台可用的指令集,把记号翻译成CPU的指令。 优化:一直在发生。 生成二进制文件:将机器代码写入目标文件。

链接:目标文件给连接器,解析外部符号等。

5.2.2 初始配置

target_compile_features() 编译器特性 target_sources() 向已定义的目标添加源 traget_include_directories() 设置预处理器的包含路径 目标的头文件路径 target_compile_definitions() 设置预处理定义 target_compile_options() 设置特定编译器的编译选项 target_precompile_headers() 预编译头文件 遵循: target_...(<target name> <INTERFACE|PUBLIC|PRIVATE> <value>)

5.2.3 管理目标源

使用target_sources给目标添加源文件,示例如下:

add_executable(main2 main2.cpp)

if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
    target_sources(main2 PRIVATE gui_linux.cpp)
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
    target_sources(main2 PRIVATE gui_windows.cpp)
endif ()

5.3 预处理配置

5.3.1 提供包含文件的路径

预处理器的基本特性是能够使用#include指令包含.h文件,包括两种形式 #include <>#include"" 通常来说, · 尖括号形式将检查标准包含目录,包括C++库和C库头文件存储在系统中的目录。 · 引号先在当前文件在的目录中搜索包含的文件,然后再目录中查找带尖括号?的目录。

CMake使用target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [item1...] ...) 来操作头文件的搜索路径。

5.3.2 通过CMake添加预处理宏定义

#define ABC
#define DEF 8

等价于

add_executable(defined definitions.cpp)

set(VAR 8)
target_compile_definitions(defined PRIVATE ABC "DEF=${VAR}")

这些标志通常以-D传递给编译器

通过宏打印git commit的SHA值

#define str(s)
#define xstr(s) str(s)  // convert definitions into c-strings

int main(){
#if defined(SHA)
    std::cout << "GIT commit: " << xstr(SHA) << std::endl;
#endif 
}

# 打印commit的SHA值
add_executable(print_commit print_commit.cpp)
execute_process(COMMAND git log -1 --pretty=format:%h OUTPUT_VARIABLE SHA)
target_compile_definitions(print_commit PRIVATE "SHA=${SHA}")

5.3.3 配置头文件

上面的方法存在一个问题,如果有多个宏的话,通过target_compile_definitions()传递变量会比较麻烦。

可以将宏收集起来,书写占位符头文件,然后在CMakeLists.txt中填充值并生成头文件。

// configure.h.in

#cmakedefine FOO_ENABLE
#cmakedefine FOO_STRING1 "@FOO_STRING1@"
#cmakedefine FOO_STRING2 "${FOO_STRING2}"
#cmakedefine FOO_UNDEFINED "FOO_UNDEFINED@"

接着,在CMakeLists.txt中填充这些宏


add_executable(configure configure.cpp)
set(FOO_ENABLLE ON)
set(FOO_STRING1 "abc")
set(FOO_STRING2 "def")
configure_file(configure.h.in configured/configure.h)
target_include_directories(configure PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

主函数如下

#include <iostream>
#include "configured/configure.h"

#define str(s) #s
#define xstr(s) str(s)

int main(){
#ifdef FOO_ENABLE
    std::cout << "FOO_ENABLE: ON" << std::endl
#endif
    std::cout << "FOO_STRING1: " << xstr(FOO_STRING1) << std::endl;
    std::cout << "FOO_STRING1: " << xstr(FOO_STRING2) << std::endl;
    std::cout << "FOO_STRING1: " << xstr(FOO_UNDEFINED) << std::endl;
}

最后,cmake会生成如下文件:

#define FOO_ENABLE
#define FOO_STRING1 "abc"
#define FOO_STRING2 "def"
/* FOO_UNDEFINED "@FOO_UNDEFINED@" */

@VAR@${VAR}被值替换,未定义的变...

5.4配置优化器

启用优化时,使用target_compile_options(<target> [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...]...])

5.4.1优化级别

优化级说明符:-O0 -O1 -O2 -O3 CMAKE_CXX_FLAGS_DEBUG等于-g CMAKE_CXX_FLAGS_RELEASE等于-O3 -DNDEBUG 内建变量的格式通常是CMAKE_<LANG>_FLAGS_<CONFIG> 还可以通过-f选项添加-finline-functions启用标志 还可以通过-fno选项添加-fnoinline-functions禁用它们

5.4.2 函数内联

5.4.3 循环展开

指将循环转换为一组语句,以达到相同的效果。空间换速度。 GCC上: -floop-unroll GCC上-O3隐式使用-floop-unroll-and-jam CLANG上: -funroll-loops

5.4.4 循环向量化

SIMD 单指令多数据

GCC在-O3级别的优化中启用循环的自动向量化,CLANG默认启用。 GCC中的启用循环向量化: -ftree-vectorize -ftree-slp-vectorize CLANG中的禁用循环向量化: -fno-vectorize -fno-slp-vectorize

5.5编译过程

5.5.1 减少编译时间

预编译头文件

target_precompile_headers(<target> <INTERFACE|PUBLIC|PRIVATE> [header1...] [<INTERFACE | PUBLICK | PRIVATE> [header2...] ...])

添加的头文件列表存储在PRECOMPILE_HEADERS目标属性中。

CMake将所有的头文件的名称放在cmake_pch.h文件中然后预编译为一个特定于编译器的二进制文件 。 使用方法

add_executable(precompiled hello.cpp)
target_precompile_headers(precompiled PRIVATE <iostream>)

//hello.cpp
int main(){
    std::cout << "Hello world" << std::endl;
}

main.cpp 文件中,不需要包含cmake_pch.h或任何其他头文件。将由 cmake 使用特定于编译器的命令行选项强制包含。

也可以重用另一个目标的预编译头文件。 target_precompile_headers(<target> REUSE_FROM <other_target>)

还有一种用法: 写一个pch.h文件,在pch.h文件中写入固定使用的一些头文件。然后在其他的文件中直接include "pch.h"文件即可。 在CMakeLists.txt中的写法如下: target_precompile_headers(<target> PUBLIC ./include/pch.h)

统一构建 针对目标的统一构建属性设置 set_target_properties(<target1> <target2> ... PROPERTIES UNITY_BUILD true) 针对文件的统一构建属性设置 set_property(SOURCE <src1> <src2>... PROPERTY UNITY_GROUP "GroupA")

使用C++20特性 module

export module helloworld;

5.5.2 查找错误

头文件的调试问题

add_executable(debug hello.cpp)
target_compile_options(debug RPIVATE -H)

用于输出包含的头文件的路径。

为调试器提供信息

CMAKE_CXX_FLAGS_DEBUG  包含 -g
CMAKE_CXX_FLAGS_RELEASE 包含 -DNDEBUG

启用release时 assert等面向调试的宏就无法起作用了。 如果一定要使用

#undef NDEBUG
#include <assert>