第46例:编译器检测——判断GCC、Clang、MSVC、Intel编译器
写跨平台代码,最头疼的是什么?我个人觉得,不是语法差异,而是编译器之间的“小脾气”。
GCC 觉得 __attribute__((packed)) 是天经地义,MSVC 却只认 #pragma pack。Clang 兼容性最好,但偶尔也会冒出几个只有它才有的警告。Intel 编译器呢?嗯,它总想用自己那套向量化魔法。
所以,在 CMake 里提前检测出当前用的是哪家编译器,就成了我们写可移植代码的第一步。说白了,就是先摸清对方的底牌,再出招。
CMake 内置的编译器变量
CMake 其实已经帮我们准备好了几个关键变量。你不需要自己去解析 CMAKE_CXX_COMPILER_ID 这个字符串,它直接告诉你答案。
| 变量名 | 含义 | 常见取值 |
|---|---|---|
CMAKE_C_COMPILER_ID |
C 编译器标识 | GNU, Clang, MSVC, Intel |
CMAKE_CXX_COMPILER_ID |
C++ 编译器标识 | GNU, Clang, MSVC, Intel |
CMAKE_Fortran_COMPILER_ID |
Fortran 编译器标识 | GNU, Intel, Flang |
CMAKE_CXX_COMPILER_VERSION |
编译器版本号 | 如 11.2.0, 14.0.0 |
这些变量在 CMake 执行 project() 命令后就会自动填充。你想想看,这比我们自己写一堆 #ifdef 要省心多了。
实战:用 if 语句判断编译器
直接上代码。这是我在一个跨平台网络库里用过的模式,后来几乎每个项目我都这么写。
cmake_minimum_required(VERSION 3.10)
project(CompilerDetectDemo LANGUAGES CXX)
# 判断编译器类型
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(STATUS "编译器:GCC (GNU)")
set(COMPILER_FAMILY "gcc")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "编译器:Clang")
set(COMPILER_FAMILY "clang")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
message(STATUS "编译器:MSVC (Visual Studio)")
set(COMPILER_FAMILY "msvc")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Intel")
message(STATUS "编译器:Intel (ICC/ICX)")
set(COMPILER_FAMILY "intel")
else()
message(WARNING "未知编译器:${CMAKE_CXX_COMPILER_ID}")
set(COMPILER_FAMILY "unknown")
endif()
# 根据编译器设置编译选项
if(COMPILER_FAMILY STREQUAL "gcc")
target_compile_options(myapp PRIVATE -Wall -Wextra -Wpedantic)
elseif(COMPILER_FAMILY STREQUAL "clang")
target_compile_options(myapp PRIVATE -Wall -Weverything)
elseif(COMPILER_FAMILY STREQUAL "msvc")
target_compile_options(myapp PRIVATE /W4 /WX)
endif()
这里有个细节:STREQUAL 是字符串比较,大小写敏感。MSVC 的 CMAKE_CXX_COMPILER_ID 就是大写的 MSVC,别写成小写。
避坑指南:Intel 编译器的“双面人生”
我曾经在给一个高性能计算项目写构建脚本时,被 Intel 编译器坑过一次。
Intel 编译器有两个版本:
- 经典版 (ICC):
CMAKE_CXX_COMPILER_ID为Intel - 新版 (ICX):基于 Clang,
CMAKE_CXX_COMPILER_ID为IntelLLVM
如果你只判断了 Intel,那用 ICX 的用户就会掉进 else() 分支。我当时就是没注意这个,结果用户反馈说“你的代码在 Intel 新编译器上编译选项没生效”。
if(CMAKE_CXX_COMPILER_ID MATCHES "Intel")
# 同时匹配 Intel 和 IntelLLVM
message("Intel 编译器家族")
endif()
用 generator-expressions 做条件编译
有时候你不想写一大段 if-else,而是想在 target_compile_options 里直接根据编译器做选择。CMake 的 generator-expressions 就是干这个的。
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Weverything>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
这种写法更紧凑,但可读性差一点。我个人习惯在简单场景用 generator-expressions,复杂逻辑还是老老实实写 if 块。
检测编译器版本号
光知道是哪家还不够,有时候还得知道具体版本。比如 GCC 4.8 之前对 C++11 的支持就很拉胯。
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "5.0")
message(WARNING "GCC 版本低于 5.0,C++14 支持可能不完整")
endif()
endif()
VERSION_LESS、VERSION_GREATER 这些比较操作符是 CMake 专门为版本号设计的,比你自己拆字符串靠谱多了。
知识体系:编译器检测的核心逻辑
下面这张图,是我梳理的编译器检测在 CMake 中的完整流程。你看一眼就能明白整个决策树。
一个完整的检测示例
最后,我把上面这些知识点揉在一起,给你一个可以直接用的模板。我在好几个开源项目里都用过类似的结构。
cmake_minimum_required(VERSION 3.15)
project(CompilerCheck LANGUAGES CXX)
# 输出编译器信息
message(STATUS "C++ 编译器 ID: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS "C++ 编译器版本: ${CMAKE_CXX_COMPILER_VERSION}")
message(STATUS "C++ 编译器路径: ${CMAKE_CXX_COMPILER}")
# 定义编译器宏,方便源码里判断
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
add_definitions(-DCOMPILER_GCC)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
add_definitions(-DCOMPILER_CLANG)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_definitions(-DCOMPILER_MSVC)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Intel")
add_definitions(-DCOMPILER_INTEL)
endif()
# 根据编译器设置标准
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "8.0")
set(CMAKE_CXX_STANDARD 17)
else()
set(CMAKE_CXX_STANDARD 14)
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 添加一个可执行文件做测试
add_executable(demo main.cpp)
add_definitions() 或 target_compile_definitions() 把编译器信息传递过去。这样你的 C++ 代码里就可以写 #ifdef COMPILER_GCC 了。
嗯,编译器检测这件事,说白了就是“知己知彼”。CMake 给了我们一套很成熟的变量和表达式,你只要记住几个关键点:CMAKE_CXX_COMPILER_ID 是核心,Intel 有新旧两套标识,版本比较用 VERSION_LESS 系列操作符。把这些用熟了,跨平台编译就不再是玄学。
公众号:蓝海资料掘金营,微信deep3321