第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_IDIntel
  • 新版 (ICX):基于 Clang,CMAKE_CXX_COMPILER_IDIntelLLVM

如果你只判断了 Intel,那用 ICX 的用户就会掉进 else() 分支。我当时就是没注意这个,结果用户反馈说“你的代码在 Intel 新编译器上编译选项没生效”。

注意:从 CMake 3.20 开始,IntelLLVM 被正式支持。如果你要兼容新旧两代 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_LESSVERSION_GREATER 这些比较操作符是 CMake 专门为版本号设计的,比你自己拆字符串靠谱多了。

知识体系:编译器检测的核心逻辑

下面这张图,是我梳理的编译器检测在 CMake 中的完整流程。你看一眼就能明白整个决策树。

CMake 编译器检测决策树 CMAKE_CXX_COMPILER_ID GNU (GCC) Clang MSVC Intel 版本 ≥ 5.0 ? 版本 ≥ 3.9 ? 版本 ≥ 19.0 ? ICC 还是 ICX ? 设置 -Wall -Wextra 设置 -Wall -Weverything 设置 /W4 /WX 设置 -qopenmp 等 最终:根据编译器家族和版本,设置对应的编译选项和宏定义

一个完整的检测示例

最后,我把上面这些知识点揉在一起,给你一个可以直接用的模板。我在好几个开源项目里都用过类似的结构。

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)
小技巧:如果你在源码里也需要判断编译器,可以用 CMake 的 add_definitions()target_compile_definitions() 把编译器信息传递过去。这样你的 C++ 代码里就可以写 #ifdef COMPILER_GCC 了。

嗯,编译器检测这件事,说白了就是“知己知彼”。CMake 给了我们一套很成熟的变量和表达式,你只要记住几个关键点:CMAKE_CXX_COMPILER_ID 是核心,Intel 有新旧两套标识,版本比较用 VERSION_LESS 系列操作符。把这些用熟了,跨平台编译就不再是玄学。


公众号:蓝海资料掘金营,微信deep3321