介绍 MSVC 的编译命令/ Zc

要深刻理解 MSVC 的 /Zc 编译命令,我们必须先聊聊微软一段长达数十年的“黑历史”。

在 20 世纪 90 年代到 21 世纪初,微软的 Windows 帝国如日中天。当时微软的 Visual C++ 编译器(MSVC)为了追求编译速度、契合 Windows 底层的特定 API,加之当时国际标准(C++98)本身也不够完善,微软在编译器中引入了大量独创的、不符合国际标准的“魔改语法”和特权。

比如:允许非左值引用绑定到临时变量、不严格区分嵌套模板的空格、老旧的宏预处理器等。

然而,随着 C++11 开启了现代 C++ 的大航海时代,国际标准变得极为严苛,GCC 和 Clang 编译器因严格遵守标准而迅速崛起。此时,微软面临一个巨大的两难境地:

  1. 如果直接强行把编译器修改为“完全符合国际标准”:全球无数依赖 VS 的企业老旧项目(如银行、军工、大型工业软件)在升级 VS 版本时会面临毁天灭地式的报错,导致无法编译。
  2. 如果继续纵容这些非标准 Bug:现代 C++ 的高端特性(如泛型编程、元编程、C++20 模块)将彻底无法在 Windows 上运行。

为了解决这个历史包袱,微软精心设计了一个**渐进式的“拨乱反正”开关——/Zc**


1. 什么是 /Zc

正如前面提到的,/Zc 是一个组合前缀

  • Z:MSVC 中关于语言特性、代码生成的系列开关前缀。
  • c:代表 Conformance(符合性),即“向 C++ 国际标准看齐”。

它的语法规则是:/Zc:特性名称[+|-](不加符号默认等同于 +)。

  • /Zc:特性:开启严格标准模式。哪怕会让你的老代码报错,也要强制按国际标准编译。
  • /Zc:特性-:关闭严格标准,退回到微软历史上的“纵容/魔改”模式,以保证老代码能跑通。

2. 核心且常用的 /Zc 选项大盘点

随着 Visual Studio 2022 到如今最新的 Visual Studio 2026 的演进,许多 /Zc 开关已经默认开启,但了解它们依然是进阶 C++ 工程师的必修课。以下是开发中最常用、最核心的选项:

1) /Zc:__cplusplus(强烈推荐手动开启)

  • 微软历史包袱:默认情况下,无论你把 C++ 标准设为 C++17 还是 C++20,MSVC 的 __cplusplus 宏永远固执地输出 199711L(C++98 的版本号)。这是为了防止极其古老的 Windows SDK 报错。
  • 开启后的改变:强制让 __cplusplus 宏正确输出当前标准的版本号(如 C++17 输出 201703L)。
  • 为什么重要:几乎所有现代跨平台开源库,都会通过这个宏来判断当前编译器支持什么特性。不开启它,第三方库常会误判。

2) /Zc:preprocessor(现代 C++ 必备)

  • 微软历史包袱:老版 MSVC 拥有一个充满 Bug、不符合标准的宏预处理器,无法正确展开变参宏,嵌套宏逻辑混乱。
  • 开启后的改变:废弃旧预处理器,启用全新的、完全符合 C++ 国际标准的预处理器。
  • 为什么重要:编写 C++20 模块、使用高级元编程库(或像 Nana、ImGui 这种深度依赖宏机制的库)时,开启它可以避免莫名其妙的宏展开错误。

3) /Zc:char8_t-(带减号,向下兼容利器)

  • 标准演进背景:C++20 引入了严格的 char8_t 类型用于 UTF-8 字符串。这导致旧代码中所有的 u8"hello" 无法再隐式转换或赋值给普通的 std::stringchar*
  • 带减号的作用禁用这一严格限制。在 C++20 环境下,依然允许将 UTF-8 字符串当做普通 char 类型处理。
  • 为什么重要:当你用 C++20 编译一个还没来得及完全适配 C++20 的第三方旧库(如之前的 Nana 库报错)时,这是最快的救场手段。

4) /Zc:wchar_t(已默认开启,历史意义重大)

  • 微软历史包袱:在最早期的 Windows 编程中,微软为了图省事,把宽字符类型 wchar_t 直接定义为了 unsigned short(无符号短整型)的别名(通过 typedef)。
  • 开启后的改变:将 wchar_t 恢复为 C++ 国际标准中规定的独立原生内置类型
  • 为什么重要:如果设为 -void foo(wchar_t)void foo(unsigned short) 就会被视为重复定义,导致严重的函数重载冲突。

5) /Zc:inline(优化编译期和程序体积)

  • 微软历史包袱:传统的 MSVC 比较纵容。如果你在头文件里声明了一个 inline 函数,但其实全篇代码都没去调用它,MSVC 依然会死板地把它编译进 .obj 文件里,最后加大 .exe 的体积。
  • 开启后的改变:严格执行标准。如果一个 inline 函数或模板函数未被使用,或者它没有定义,编译器会直接将其剔除。
  • 为什么重要:可以显著加快链接速度,并让最终生成的二进制文件更小、更干净。

3. 在现代开发中,应该如何对待 /Zc

如果你在使用 CMake 进行现代 C++ 开发,微软其实已经意识到了天天让开发者手动配这些选项非常痛苦。

从近几年的现代 MSVC 开始,微软引入了一个“终极外挂”参数:/permissive-(意为“关闭宽容模式”)。
当你开启了 /permissive- 时,它会自动在幕后把绝大多数 /Zc 选项(如 /Zc:referenceBinding/Zc:strictStrings 等)一键全部调成严格符合标准的状态。

因此,在现代 CMake 中,针对微软编译器的标准标准配置通常长这样:

if(MSVC)
    # 1. 一键开启全面严格标准模式(相当于开启了一大堆 /Zc)
    add_compile_options(/permissive-)
    
    # 2. 依然需要手动补上的漏网之鱼(由于历史原因,/permissive- 没包含它们)
    add_compile_options(/Zc:__cplusplus)
    add_compile_options(/Zc:preprocessor)
endif()

通过这一套组合拳,你就把原本满是历史包袱的 MSVC,彻底伪装成了一个像 GCC 和 Clang 一样纯正、现代的、无 Bug 的标准 C++ 编译器!

小花絮 permissive

1. permissive 的音标与本义

  • 英音(UK):/pəˈmɪs.ɪv/
  • 美音(US):/pɚˈmɪs.ɪv/

📖 词源与本义

该词源自拉丁语动词 permittere(允许、放任),在日常英语中它是一个形容词:

  • 字面本义“放任的、纵容的、宽容的、听之任之的”
  • 日常语境:通常用来形容一种管理风格或社会风气。例如,permissive parents 指的是“对孩子百依百顺、不加管束的父母”;a permissive society 指的是“道德行为极其开放、自由放任的社会”。

2. 在上文(MSVC 编译器)中的具体含义

在微软 MSVC 编译器的语境下,/permissive 被借用为一个技术术语,用来形容编译器的“宽容度”。

🟢 /permissive(默认或老版本状态):宽容模式

  • 含义:此时编译器就像一个“睁一只眼闭一只眼的老好人老师”。
  • 表现:即使你的代码违反了现代 C++ 国际标准的规定,但只要它是历史上 Windows 特有的写法,或者属于某种早期的非标准 Bug 语法,编译器都会选择纵容它、放过它,允许它编译通过而不报错。

🔴 /permissive-(末尾带减号):关闭宽容模式(即:严格标准模式)

  • 含义:我们在上文中用到的 /permissive- 带有减号,表示“封杀/关闭这种纵容的态度”。
  • 表现:编译器瞬间变成了一个“严厉、死板的国际考官”。它会剥夺 MSVC 历史上的所有“特权”和“纵容”,完全按照 C++ 国际标准(ISO C++)的法律法规来逐行审查你的代码。任何投机取巧、不符合跨平台标准的语法都会被直接无情地判定为严重错误(Error)。

总结

微软引入 /permissive- 这个名字非常带有自嘲的幽默感。加了这个参数,就是命令编译器:“收起你过去几十年对 Windows 老旧代码的‘溺爱与纵容’,给我按照国际标准严格执法!”

posted @ 2026-06-18 13:56  立体风  阅读(14)  评论(0)    收藏  举报