☰
CMake 语言标准与扩展标志选择改进:CMP0128 策略深度解读与迁移指南
2026/10/10 5:45:08 网站建设 项目流程
  • 构建工具
  • 开发工具
  • CLI

【免费下载链接】CMake

Mirror of CMake upstream repository

项目地址:https://gitcode.com/gh_mirrors/cm/CMake
点击查看免费下载

导读:CMP0128 是 CMake 3.22 引入的一项行为策略,它系统性地改进了 CMake 对语言标准(<LANG>_STANDARD)与编译器扩展(<LANG>_EXTENSIONS)相关编译标志的选取逻辑,使<LANG>_EXTENSIONS目标属性的默认值从固定ON改为遵循编译器的真实默认行为,并只在确有必要时才追加影响标准模式的标志。本文以 CMP0128 的官方文档为骨架,结合本仓库(CMake 官方源码镜像)中 cmStandardLevelResolver.cxx 的实现与 CompileFeatures 测试套件,完整对比 OLD/NEW 两种行为的差异、列出需要迁移的三种典型代码场景,并给出可复制的策略设置与警告控制方法,帮助读者理解并平稳迁移到新的语言标准处理逻辑。

一、CMP0128 是什么:策略背景与引入动机

CMP0128 于 CMake 3.22 版本引入,策略编号在源码注册表中对应的描述为 "Selection of language standard and extension flags improved"(语言标准与扩展标志的选择得到改进),见 Source/cmPolicies.h:

SELECT(POLICY, CMP0128, \ "Selection of language standard and extension flags improved.", 3, \ 22, 0, WARN)

其中3, 22表示该策略随 3.22 版本引入,0为该策略的默认策略版本(即对未显式设置的项目不强制启用),WARN表示策略状态未设置时产生警告。从 Help/policy/include/STANDARD_ADVICE.rst 可以确认:该策略在未设置时默认不产生警告("doesnotwarn by default"),并使用OLD行为。

CMake 引入这一策略的原因,在于旧版(OLD)语言标准与扩展标志处理逻辑存在一系列与实际编译器行为不符的问题:

  • <LANG>_EXTENSIONS目标属性在CMAKE_<LANG>_EXTENSIONS未设置时一律回退为ON,但绝大多数编译器(如 GNU、Clang 的gnu++模式)默认本就启用扩展,ON的硬编码既不反映真实默认值,也掩盖了潜在的可移植性问题——正如 cmake-compile-features(7) 手册 所指出的:"<LANG>_EXTENSIONS过去默认是ON。参见 CMP0128",且多数编译器默认启用扩展可能会暴露用户代码或第三方依赖头文件中的可移植性缺陷;
  • 当<LANG>_STANDARD未设置或恰好与编译器默认标准一致时,扩展的启用/禁用实际上不生效,导致属性设置与编译标志脱节;
  • 只要<LANG>_STANDARD已设置且<LANG>_STANDARD_REQUIRED为OFF,旧逻辑就无条件追加一个标准模式标志,即使编译器默认模式已满足要求,产生冗余标志(例如在 GCC 默认gnu++17的编译器上仍多传一个-std=gnu++17)。

CMP0128 的NEW行为正是针对上述问题,让 CMake 依据编译器真实默认值(CMAKE_<LANG>_STANDARD_DEFAULT与CMAKE_<LANG>_EXTENSIONS_DEFAULT)来初始化目标属性、正确同步扩展开关,并仅在必要时追加标志。

二、NEW 行为与 OLD 行为逐条对比

2.1 NEW 行为(策略设置为 NEW)

当项目将该策略设置为NEW时,CMake 会:

  1. 按编译器默认初始化扩展属性::prop_tgt:<LANG>_EXTENSIONS目标属性优先初始化为CMAKE_<LANG>_EXTENSIONS(若该变量已设置),否则回退到CMAKE_<LANG>_EXTENSIONS_DEFAULT(编译器的默认扩展模式)。
  2. 正确同步扩展开关:当<LANG>_STANDARD未设置,或其所请求的标准恰好被编译器默认标准满足时,<LANG>_EXTENSIONS的启用/禁用会真实反映到编译标志上。
  3. 按需追加标志:影响标准模式的标志(如-std=gnu++11)只在“确有必要”时才会被追加,即:所请求标准低于编译器默认标准,或扩展模式与编译器默认不一致。

2.2 OLD 行为(策略未设置时的兼容行为)

OLD行为保留了历史逻辑:

  1. 扩展属性默认固定为 ON::prop_tgt:<LANG>_EXTENSIONS初始化为CMAKE_<LANG>_EXTENSIONS(若设置),否则回退为ON——而非编译器的默认扩展模式。
  2. 无条件追加标志:只要<LANG>_STANDARD已设置且<LANG>_STANDARD_REQUIRED为OFF,就总是追加一个标准模式标志。
  3. <LANG>_STANDARD未设置时行为错乱:
    • 即使<LANG>_EXTENSIONS为OFF,也不会真正禁用扩展;
    • 即使<LANG>_EXTENSIONS为ON,也无法真正启用扩展(唯一的例外是IAR编译器)。

2.3 源码级印证:核心决策逻辑

上述行为差异在 Source/cmStandardLevelResolver.cxx 的StandardLevelComputer::GetCompileOptionDef()(约 L73-L233)中得到了完整实现。该函数逐条体现 NEW/OLD 分歧点:

扩展默认值的差异(L88-L102):

cmPolicies::PolicyStatus const cmp0128{ makefile->GetPolicyStatus( cmPolicies::CMP0128) }; bool const defaultExt{ makefile ->GetDefinition(cmStrCat("CMAKE_", this->Language, "_EXTENSIONS_DEFAULT")) .IsOn() }; bool ext = true; if (cmp0128 == cmPolicies::NEW) { ext = defaultExt; // NEW:跟随编译器默认扩展模式 } if (cmValue extPropValue = target->GetLanguageExtensions(this->Language)) { ext = extPropValue.IsOn(); // 目标属性显式设置时以属性为准 }

<LANG>_STANDARD未设置时的分歧(L106-L142):

  • NEW路径:仅当扩展模式与编译器默认不一致时,才返回CMAKE_<LANG>_<defaultStd>_EXTENSION_COMPILE_OPTION(如-std=gnu++17),即“按需添加扩展标志”;而OLD路径在ext为真时无条件返回CMAKE_<LANG>_EXTENSION_COMPILE_OPTION(如-std=gnu++11),并且即使ext为假也“什么都不做”(不真正禁用扩展)。
  • OLD路径下若检测到ext != defaultExt且警告开关开启,会发出策略警告,如 "compiler extensions won't be enabled/disabled"(L115-L134)。

冗余标志判定(L166-L217):当请求的标准与编译器默认标准一致且扩展模式一致时,NEW直接返回空字符串(不追加任何标志),而OLD会继续发出 "unnecessary flags for language standard or compiler extensions may be added" 的警告并追加标志;当请求标准更旧或扩展模式不匹配时,NEW使用<严格比较(stdIt < defaultStdIt),OLD使用<=(stdIt <= defaultStdIt),因此OLD即使标准恰好等于默认值也会加标志。

值得留意的是,源码注释(L71-L72)指出GetCompileOptionDef的逻辑与GetEffectiveStandard(L235-L334)是相互镜像的,修改一处必须同步另一处——这保证了“实际生效的标准”与“追加的标志”始终一致。

三、三个必须迁移的代码场景

官方文档明确指出,以下三种情况下的代码需要针对NEW行为进行更新:

场景 1:被 CMake 标志“覆盖”的手工标准标志开始生效

旧行为:项目在CMAKE_<LANG>_FLAGS等位置手工添加了-std=标志,而 CMake 在编译器检测阶段并不使用这些标志;同时 CMake 自身又追加了一个默认标准的标志,从而掩盖/覆盖了手工标志。

新行为:由于 CMake 检测到编译器默认标准已合适、不再追加自己的标志,项目手工添加的-std=标志就会真实地参与编译,可能引入意料之外的编译模式。

迁移建议(二选一):

  • 改用:prop_tgt:<LANG>_STANDARD与:prop_tgt:<LANG>_EXTENSIONS属性来表达标准与扩展需求,让 CMake 负责选择标志,删除手工标志;
  • 或者确保手工指定的标志在编译器检测阶段就生效(例如在工具链文件里通过CMAKE_<LANG>_FLAGS_INIT设置,参见下文第五节),使检测出的CMAKE_<LANG>_STANDARD_DEFAULT与CMAKE_<LANG>_EXTENSIONS_DEFAULT与最终编译命令一致。

场景 2:未设置<LANG>_STANDARD却禁用了扩展

旧行为:项目只设置set_property(TARGET t CXX_EXTENSIONS OFF)而未设置CXX_STANDARD,但 CMake 实际上不会真正禁用扩展(参见 2.2 节的 OLD 行为第 3 点)。

新行为:扩展会被真正禁用。

迁移建议:如果代码确实依赖扩展特性,则应更新为“不要禁用扩展”(即移除CXX_EXTENSIONS OFF或将其设为ON),而不是依赖旧行为中“禁用无效”的巧合。

场景 3:<LANG>_STANDARD恰好被编译器默认满足时的扩展开关

旧行为:当<LANG>_STANDARD请求的标准已被编译器默认标准满足时,即使设置了<LANG>_EXTENSIONS的 ON/OFF,CMake 实际上也不会真正启用/禁用扩展。

新行为:扩展模式会被真实地启用或禁用。

迁移建议:此类代码应显式设置正确的扩展模式,明确表达意图,而不是依赖旧逻辑的“不生效”。

四、如何设置 CMP0128 策略

4.1 通过 cmake_minimum_required 隐式设置(推荐)

cmake_minimum_required(VERSION)会隐式调用cmake_policy(VERSION),从而将运行版本之前引入的所有策略设置为NEW(见 Help/command/cmake_minimum_required.rst 与 Help/command/cmake_policy.rst)。由于 CMP0128 引入于 3.22,项目只要声明最低版本不低于 3.22 即可自动启用NEW行为:

cmake_minimum_required(VERSION 3.22) # CMP0128 自动置为 NEW project(MyProject LANGUAGES CXX)

即使最低版本低于 3.22,也可以使用cmake_minimum_required(VERSION 3.10...3.22)这种带policy_max的形式,将策略上限提升到 3.22,从而在保留较低最低版本的同时启用新行为:

cmake_minimum_required(VERSION 3.10...3.22)

4.2 通过 cmake_policy 显式设置

项目可以随时用cmake_policy(SET CMP0128 NEW|OLD)显式指定行为(见 Help/command/cmake_policy.rst):

cmake_policy(SET CMP0128 NEW) # 启用新行为 # 或 cmake_policy(SET CMP0128 OLD) # 保留旧行为,用于兼容尚未迁移的旧代码

也可以通过cmake_policy(GET CMP0128 <variable>)查询当前策略状态:策略已设置时<variable>取值为OLD或NEW,未设置则为空。

需要注意:策略遵循“策略栈”机制(Help/command/cmake_policy.rst)。每个子目录、include()与find_package()加载的脚本(除非带NO_POLICY_SCOPE)都会自动压入新的策略栈条目;cmake_policy(SET)只影响当前栈顶。若想在函数/宏内部临时修改策略并确保退出时恢复,推荐使用block(SCOPE_FOR POLICIES)(CMake 3.25+)而非手工cmake_policy(PUSH)/POP配对。

4.3 一个典型迁移示例

假设项目过去依靠 OLD 行为“禁用扩展无效”来隐藏问题,现在要显式启用 CMP0128 并固定标准:

cmake_minimum_required(VERSION 3.22) project(Cmp0128Demo LANGUAGES CXX) cmake_policy(SET CMP0128 NEW) add_library(demo demo.cpp) # 明确指定标准与扩展模式,替代手工 -std= 标志 set_target_properties(demo PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF # 在 NEW 行为下真实生效 ) # 或使用编译特性元特性(效果等价且语义更清晰) # target_compile_features(demo PUBLIC cxx_std_17)

在CXX_EXTENSIONS OFF且CXX_STANDARD 17的组合下,NEW行为会为 GNU/Clang 类编译器追加-std=c++17(非扩展模式),而CXX_EXTENSIONS ON时追加-std=gnu++17。

五、与编译器检测的联动:工具链文件中的标志会影响默认值

官方文档强调了一个重要前提:如果影响标准模式的编译器标志在编译器检测阶段就被使用(例如在工具链文件 cmake-toolchains(7) 中通过CMAKE_<LANG>_FLAGS_INIT设置),那么它们会影响检测出的默认标准(CMAKE_<LANG>_STANDARD_DEFAULT)与默认扩展模式(CMAKE_<LANG>_EXTENSIONS_DEFAULT)。

这一点直接对应第 3 节“场景 1”的第二种迁移建议:若项目坚持手工管理标志,就应把这些标志提前到编译器检测阶段注入,例如:

# 工具链文件 toolchain.cmake(在 CMAKE_<LANG>_FLAGS_INIT 中注入标准标志) set(CMAKE_CXX_FLAGS_INIT "-std=c++17")

这样检测阶段得到的CMAKE_CXX_STANDARD_DEFAULT会如实反映最终编译模式,CMP0128 的NEW逻辑据此判断“默认已合适”而不再追加冗余标志,避免出现“检测期与构建期模式不一致”的问题。

相关变量的语义如下:

  • CMAKE_<LANG>_STANDARD_DEFAULT(Help/variable/CMAKE_LANG_STANDARD_DEFAULT.rst):编译器的默认语言标准,若编译器没有“标准级别”概念则为空;用于判断请求的标准是否已被默认满足。
  • CMAKE_<LANG>_EXTENSIONS_DEFAULT(Help/variable/CMAKE_LANG_EXTENSIONS_DEFAULT.rst):编译器的默认扩展模式,只读变量,修改它是未定义行为;在CMAKE_<LANG>_EXTENSIONS未设置时作为:prop_tgt:<LANG>_EXTENSIONS的默认值(见 Help/prop_tgt/LANG_EXTENSIONS.rst)。

六、警告控制:CMAKE_POLICY_WARNING_CMP0128

CMP0128 属于“默认不产生警告”的策略。官方文档(Help/variable/CMAKE_POLICY_WARNING_CMPNNNN.rst)确认CMAKE_POLICY_WARNING_CMP0128变量用于控制该策略的警告。

该变量不应由项目在 CMake 代码中设置,而应由运行 CMake 的开发者通过缓存变量注入以显式启用警告:

cmake -DCMAKE_POLICY_WARNING_CMP0128=ON -S . -B build

或者使用cmake --debug-output、cmake --trace、cmake --trace-expand命令行选项,也会启用该警告。在WARN状态下,源码实现会在检测到扩展模式与编译器默认不一致时发出 "compiler extensions won't be enabled/disabled" 的提示;在标准与默认一致却仍将追加标志时发出 "unnecessary flags ... may be added" 的提示(L173-L180)。

需要强调的是,OLD行为按定义已被弃用(见 Help/policy/include/DEPRECATED.rst):“策略的 OLD 行为从定义上已被弃用,可能在未来的 CMake 版本中被移除”,因此长期来看项目应迁移到NEW行为,OLD仅作为过渡期的兼容手段。

七、测试与验证:CMP0128 行为有据可查

本仓库的 Tests/RunCMake/CompileFeatures 目录下提供了一整套针对 CMP0128 的回归测试,逐一覆盖文档描述的各个行为分支:

测试文件验证内容
CMP0128Common.cmake公共模板:enable_language()、剥离隐藏标记以暴露真实编译命令、创建foo库
CMP0128NewExtensionsStandardDefault.cmakeNEW行为:标准恰好等于编译器默认、扩展取反时的标志选取
CMP0128NewExtensionsStandardUnset.cmakeNEW行为:<LANG>_STANDARD未设置、仅反转扩展模式
CMP0128NewNoUnnecessaryFlag.cmakeNEW行为:标准与扩展均等于默认时不追加冗余标志
CMP0128OldSameStandard.cmakeOLD行为:标准等于默认时仍追加标志
CMP0128WarnMatch.cmake / CMP0128WarnUnset.cmakeWARN状态下的警告输出(对应 stderr 基线文件)

其中CMP0128WarnUnset.cmake显式做了cmake_policy(SET CMP0128 OLD)并set(CMAKE_POLICY_WARNING_CMP0128 ON),随后仅设置CMAKE_<LANG>_EXTENSIONS为相反值——正好复现了文档中“未设置<LANG>_STANDARD时扩展开关失效”的 OLD 行为,并验证警告文本。

此外,try_compile子系统也与 CMP0128 联动:Source/cmCoreTryCompile.cxx 在生成try_compile内层项目时会检查外层项目的策略状态,若外层不是NEW则在生成的CMakeLists.txt中写入cmake_policy(SET CMP0128 OLD),保证try_compile/CheckSourceCompiles等探测行为与外层项目一致。对应测试见 Tests/RunCMake/try_compile/CMP0128-common.cmake 与 CMP0128-NEW.cmake(后者显式cmake_policy(SET CMP0128 NEW),再借助CMAKE_REQUIRED_FLAGS传入-std=c++11,验证NEW行为下 CMake 会按需把标准标志追加在用户标志之后)。

八、相关属性与变量的全景视图

为了让读者对 CMP0128 的作用面有完整认识,以下是与之直接相关的目标属性与变量清单:

名称类型与 CMP0128 的关系
:prop_tgt:<LANG>_EXTENSIONS(C/CXX/CUDA/HIP/OBJC/OBJCXX)目标属性CMP0128 决定其默认值来源:CMAKE_<LANG>_EXTENSIONS→CMAKE_<LANG>_EXTENSIONS_DEFAULT(NEW)或ON(OLD)
:prop_tgt:<LANG>_STANDARD(C/CXX/CUDA/HIP/OBJC/OBJCXX)目标属性指定请求的语言标准;CMP0128 决定“标准被默认满足”时是否仍需追加标志
:prop_tgt:<LANG>_STANDARD_REQUIRED目标属性为ON时标准成为硬性要求,不满足则致命错误;为OFF/未设置时 CMP0128 的“按需追加”逻辑才会介入
CMAKE_<LANG>_EXTENSIONS变量目标创建时初始化<LANG>_EXTENSIONS的默认值(若设置),见 Help/variable/CMAKE_LANG_EXTENSIONS.rst
CMAKE_<LANG>_EXTENSIONS_DEFAULT变量(只读)编译器默认扩展模式,NEW 行为的回退值
CMAKE_<LANG>_STANDARD变量目标创建时初始化<LANG>_STANDARD的默认值(若设置),见 Help/variable/CMAKE_LANG_STANDARD.rst
CMAKE_<LANG>_STANDARD_DEFAULT变量编译器默认标准,用于“是否被默认满足”的判断
CMAKE_POLICY_WARNING_CMP0128变量控制 CMP0128 的警告开关(默认不警告)
CMAKE_<LANG>_FLAGS_INIT变量工具链文件中注入标志的入口;会影响检测出的默认标准与扩展模式

扩展目标属性与标准属性在官方文档中的完整定义见 Help/prop_tgt/LANG_EXTENSIONS.rst 与 Help/prop_tgt/LANG_STANDARD.rst。关于标准与扩展在target_compile_features、COMPILE_FEATURES生成器表达式(cmake-generator-expressions(7))等更广泛编译特性体系中的使用,可进一步阅读 cmake-compile-features(7) 手册(其中 L120-L128 明确指出<LANG>_EXTENSIONS在 NEW 行为下默认跟随编译器默认值,而这可能暴露可移植性问题,正是迁移 CMP0128 后需要关注的重点)。

九、总结与迁移检查清单

CMP0128 让 CMake 的语言标准与扩展标志处理从“启发式猜测”走向“基于编译器真实默认值”的精确建模。迁移到NEW行为后,<LANG>_EXTENSIONS会真实反映编译器的默认扩展模式,扩展开关在所有情况下都真正生效,且冗余的标准模式标志被消除。对于依赖旧行为“标志被覆盖”“禁用扩展无效”的项目,务必按第三节的三类场景逐项检查并修正。

迁移建议遵循以下检查清单:

  1. 将cmake_minimum_required(VERSION ...)提升到 3.22 或使用...3.22的policy_max形式,让 CMP0128 进入NEW状态;
  2. 全局搜索手工添加的-std=/-std:iso等标准模式标志,改用:prop_tgt:<LANG>_STANDARD+:prop_tgt:<LANG>_EXTENSIONS,或将其迁移到工具链文件的CMAKE_<LANG>_FLAGS_INIT;
  3. 检查所有CXX_EXTENSIONS/C_EXTENSIONS等设置,确认扩展开关在NEW行为下按预期真实生效(尤其注意“未设置<LANG>_STANDARD却禁用扩展”与“标准被默认满足却切换扩展”两类旧代码);
  4. 使用cmake -DCMAKE_POLICY_WARNING_CMP0128=ON开启警告,结合--debug-output/--trace检查是否还有未收敛的 OLD 行为依赖;
  5. 注意try_compile探测行为会继承外层项目的 CMP0128 状态(见 Source/cmCoreTryCompile.cxx),若探测结果异常,先核对内外层策略是否一致。
  • 构建工具
  • 开发工具
  • CLI

【免费下载链接】CMake

Mirror of CMake upstream repository

项目地址:https://gitcode.com/gh_mirrors/cm/CMake
点击查看免费下载

相关推荐

上一篇:高效实现Windows资源管理器STL文件预览的智能方案
下一篇:py12306分布式架构深度解析:多节点集群设计与高并发实现原理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询