整理一下关于C++编译器的笔记,这部分由于不是科班出身,没学过编译原理之类的课程,细节总是搞不懂, 本文只是基于网络搜集到的各种零散的描述进行整理,不保证正确性!!!

在本文中只关注x64平台,Windows或Linux(Ubuntu)系统的C/C++编译环境,并且不考虑交叉编译等问题。 除了系统原生的MSVC和GCC,重点关注两个概念:

  • MinGW,MinGW-W64,MSYS...
  • LLVM,clang

将它们并列是不合适的,因为它们不是一个层面上的东西,但是这两个确实是我在理解C++编译环境配置时遇到的最大困难。

基本概念

c 语言的标准分成两部分:

  • 语法,指导我们应该怎么编程,在源文件中必须满足某些规则
  • 标准库(头文件),相对于系统层面向 c 语言程序提供的一些基础接口,例如printf等,但语法标准只是规定了接口的形式,并没有具体的实现

对 c 语言的支持,也分成两个部分:

  • c 编译器,从源代码得到可执行文件和库文件
  • c 标准库的具体实现,以库文件的形式提供最基础的功能(以及对应的头文件),供其他程序使用

由于Liunx系统和windows系统都是基于c/c++编写的,在系统中实际提供的不仅仅是c标准库,还包括了与系统相关的几乎所有函数接口,因此称为c运行时更加合适,几乎所有的程序都直接或间接地依赖它。

在 Linux 平台上,最早采用的 c 运行时实现叫 libc,但是后来逐渐被 GNU 的 glibc 替代。 现在的 Linux 使用的 c 运行时通常都是 GNU 的 glibc,它包括一组头文件和一组库文件(libc.so.6libm.so等),以动态库的形式提供给 c 程序调用。 GCC 除了需要 glibc 之外,实际上无论编译时还是运行时都需要一个 gcc 底层库 libgcc,一个版本 gcc 编译的程序常常不能在装有另一个版本 gcc 的平台上运行,主要是因为 libgcc 版本冲突。

因为glibc太重要了,所以gcc和glibc并没有绑定在一起,不像下面的c++编译器和c++标准库,通常是打包在一起的。

在一些嵌入式平台上,使用的是定制的 c 运行时,可能进行了针对性的优化,以及语法功能的扩展或删减,例如 uClibc 等, 甚至很多嵌入式平台并没有完全意义上支持 c 语言,可能只有对应的 c 编译器,但是并没有完整的 c 标准库。

c++ 和 c 语言类似,标准包括语法和标准库两部分。

在 Linux 平台上,默认采用的 c++标准库实现都是 GNU 的 libstdc++,在安装 gcc 时,必然附带安装对应版本的 libstdc++,但是 gcc 和 glibc 并没有绑定到一起,因为 glibc 是系统级的,使用范围更广,更基础。

还有一个广泛采用的 c++标准库实现,是 LLVM 的 libc++,通常会配置 LLVM 的 clang 编译器使用,在 MacOS 和 iOS 广泛使用。(clang 可以基本兼容和替换 gcc 的使用,包括常见的编译选项都支持,MacOS 上自带的 gcc 其实就是 clang 换了个名字,关于LLVM和clang的讨论见下文)

在 windows 平台的内容主要参考微软官方文档, 值得注意的是,C/C++的运行时或标准库在 windows 上并没有被严格拆分开。

早期的c运行时被称为 Microsoft's C/C++ Runtime Library(MSVCRT),大约在 win10/VS2015 之后,微软对c运行时进行了重构,发布了新的通用CRT(UCRT)。 UCRT 作为 Windows 10 和更高版本系统的一部分直接提供,在文件系统中主要对应下面的内容

  • 静态库版本:libucrt.lib
  • 动态库版本:ucrt.lib + ucrtbase.dll

支持C++的主要是 vcruntime (VCRT),包含 Visual C++ 需要的特定代码: 异常处理和调试支持、运行时检查和类型信息、实现的详细信息和某些扩展的库函数,在文件系统中主要对应下面的内容

  • 静态库版本:libvcruntime.lib
  • 动态库版本:vcruntime.lib + vcruntime<version>.dll

除了这几个最主要的概念,实际上具体的标准库以及它们之间的关系非常复杂,这里略去,可以参考微软官方文档。

在与GCC对比时,微软的编译工具链通常被称为 Microsoft Visual C++(MSVC),具体对应的是cl.exe以及其他附属工具和库。

主流编译环境

C/C++的编译器有很多种实现,最简单的甚至只需要几千行就可以实现一个C语言编译器的子集(c4编译器), 但是在讨论C++的跨平台编译时,我们通常考虑的就是三种主流编译器(以及配套的工具链):

  • Windows,MSVC
  • Linux,GCC
  • Linux,Clang

它们的背后分别是微软公司,GNU以及LLVM,这三家同时也是C++标准委员会的主角。

这三家编译器虽然遵照的是同一份公开的C++标准,但是各自的实现并不完全一致,这带来了无穷无尽的问题:

  • 很可能出现在一个编译器中顺利编译,但是在另一个编译器中报错的情形,例如:
    • 标准头文件的包含关系:有的编译器处理时不需要加上某个标准头文件,其他地方已经自动包含了;但有的编译器就会报错
    • 在涉及模板以及部分较新的语法时,它们的实现进度不同,有的编译器即使最新版也支持的很不完整(例如C++20的module),根本无法编译
  • 对于编译过程中的警告和编译错误信息的反馈,详细程度也是完全不一样的,在可读性方面clang的表现通常更好。
  • 即使在不同编译器中都顺利编译了,它们得到的程序也是很不相同的,例如
    • 编译器对代码的优化程度不一样,运行效率不一样
    • 万一程序涉及到了未定义行为(即语法标准没有明确规定的行为),不同编译器的处理并不一致,可能导致不同的结果

这三种编译器在各自的系统上安装和配置是很容易的,下面依次介绍。

Visual Studio 2022 无脑从官网安装即可,在安装组件中选择 Desktop development with C++(C++桌面开发)即可。

通常的使用都是在Visual Studio的图形界面中,但是也支持命令行窗口,在安装完成VS之后,开始菜单可以找到如下几个快捷方式:

  • Developer Command Prompt for VS 2022
  • Developer PowerShell for VS 2022

这两个的主要作用就是分别打开cmd和powershell,但是在启动时自动进行了MSVC需要的环境配置,我们可以在其中通过纯命令的方式形式使用。

关于Visual Studio和msvc的版本号的对应关系很复杂,有很多平行的版本体系,可以参考wiki,例如Visual Studio 2017-2022均对应大版本号msvc14。

Linux安装gcc和g++就更简单了,例如Ubuntu两行搞定

1
2
sudo apt install g++
sudo apt install gcc

测试一下

1
2
3
4
5
which gcc
gcc -v

which g++
g++ -v

Linux安装clang和clang++也可以直接apt install

1
2
sudo apt install clang++
sudo apt install clang

但是官方源提供的gcc/clang版本都比较低,需要额外的配置去别的地方下载最新版的,例如当前的最新版是gcc13和clang18,具体的做法在WSL2安装记录中有,因此不再重复。

在编译程序时 clang++ 还是采用当前Linux系统默认的C++标准库 libstdc++,而不是LLVM提供的libc++。(通常也没有附带下载libc++)

MinGW

理解MinGW以及MinGW-W64这些类似名词是一个漫长的过程,参考了很多的博客文章。

Cygwin和MinGW

故事从 cygwin 开始,Cygwin项目是基于win32 API在用户态模拟UNIX,Cygwin的核心是 cygwin1.dll,由 cygwin1.dll 提供POSIX API,并基于cygwin1.dll移植了大量GNU、BSD的软件包,这些软件包的源码在cygwin环境中重新编译的时候,都会自动链接到cygwin1.dll,编译的二进制结果仍然是Windows PE格式,在运行时依赖cygwin1.dll。cygwin本身无法支持Linux ELF格式二进制程序。

MinGW(Minimalist GNU for Windows) 是 GNU 工具(包括编译器 GCC 和 GNU binutils 和调试器 GDB 等)在 Win32 上的一个移植,MinGW 除了GCC,GDB等工具外,还提供了一个适配于 Win32 的运行时环境,其中 C 标准库实现的二进制文件直接用微软随 Windows 分发的 MSVCRT ,MinGW 自己的运行时库依赖于MSVCRT和其它系统库。

MinGW最早是从 Cygwin 里 fork 出来的,但后续版本很快完全脱离了 Cygwin,可以认为没有啥关系,并且MinGW和Cygwin从设计目标来说就是有区别的:

  • MinGW 的目标很小,能在windows上用gcc就行,用户的源代码可以部分修改或重新,强调简洁和性能,并不打算造一个POSIX兼容层。由于 gcc 在工作时不是孤立存在的,gcc本身就调用了很多其他工具,因此 MinGW 也顺便提供了一些配套工具。
  • Cygwin 的目标很大,希望在Linux上的源代码在这里几乎不需要改动,重新编译就可以跑,因此重视POSIX兼容性,为了达到这个目的 Cygwin 必然需要魔改 gcc 等编译工具链。因为Cygwin多了统一的转换层导致性能可能略低。关于POSIX兼容性的一个例子,能不能使用fork()函数,Cygwin可以,但是MinGW不行。

MinGW GCC 需要依赖 MinGW 运行时以及 libgcc 和其它系统库。编译出来的程序在运行时仍然需要依赖这些库,所以才会写死在默认 specs 里(可以用 gcc -dumpspecs 查看),免得用户随便编译链接个程序还得手动指定一大堆 -l 选项。最初的 MinGW 只考虑到了32位,目标平台三元组为 i386-pc-mingw32。

MinGW-W64及其发行版

接下来就是从32位到64位的重生,MinGW-W64 的主要维护者 Kai Tietz ,因为工作需要重新实现了提供 x64 支持的 MinGW,但是被 MinGW 拒绝合并,于是 fork 之后发展为单独的项目,这就是 MinGW-W64 的由来。

在当前我们所有讨论的MinGW都默认是MinGW-W64,鉴于有时会省略W64,很容易造成混乱,因此最初的那个 MinGW 还是直接忘记它吧,别一不小心进入了它的官网 MinGW.org ,通常应该进这个:MinGW-W64.org

虽然名称带有64位,但 MinGW-W64 仍然是同时支持 32 位和 64 位的,甚至有的还支持 32 位和 64 位的交叉编译。 支持 64 位的 MinGW-W64 对应三元组是 x86_64-w64-mingw32。

就像Linux内核一样,MinGW-W64 是致力于特定软件包(MinGW-W64 运行时库)进行开发的项目,但是并不着力于提供一个开箱即用的环境。正如 Linux 内核和各种具体的Linux发行版(Ubuntu,Centos等)的关系,在 MinGW-W64 这里也有 MinGW-W64 发行版的概念。

很多人基于 MinGW-W64 运行时库进行进一步的定制封装,得到的产物称为MinGW-W64发行版,实质就是给用户提供一个开箱即用的环境。通常下载它提供的压缩包并解压到任意位置,然后将 gcc 等所在的bin目录添加到Windows 环境变量 PATH,就可以正常使用。

在MinGW-W64官网的下载页面中,Pre-built toolchains and packages 部分提供了很多开箱即用的 MinGW-W64 发行版。Source部分提供的则是 MinGW-W64 自己的源码,这些源码并不是开箱即用的,发行版会基于这些源码进行打包。例如:

  • mingw-builds:这是中文互联网中推荐最多的MinGW发行版,这个项目早期部署在sourceforge.net,直接提供安装程序。后面合并到 MinGW-W64 项目中作为官方发行版,原始网址就停更了,因此sourceforge.net下载到的发行版的gcc等版本很低。

  • WinLibs.com:相比于其他发行版的简陋,这个发行版至少还有一些文档说明,包括怎样选择发行版提供的某一个具体版本,这些选择对于其他发行版也是通用的,选项说明见下文。

  • LLVM-MinGW:这也是一个很常见的发行版,因为涉及到LLVM,留着后面再说。

这些发行版说实话很多都不是非常正式,可能是几个人简单鼓捣并维护的。 除了用了同样的 MinGW-W64 运行时库,并且让你可以开箱即用:输入gcc -v成功输出版本信息, 这些发行版之间有各种各样的区别(打包的工具的版本差异相比来说简直是微不足道的),例如有的除了gcc,还附带gfortran等其他GCC广义上包括的其他语言编译器;有的除了gcc,还提供clang。(见下文)

很多发行版实际上提供了很多不同的版本供我们选择,对应的是它在搭建环境及打包发行版时作的不同处理(这是一个很复杂的问题,是C/C++混乱问题的延续),通常也体现在提供的压缩包名称中:

  • 线程模型:(对于C++实现而言的)
    • POSIX(与其他平台的最佳兼容性,最好用这个,后面clang也都是这个)
    • WIN32(本机 Windows 线程 API ,但缺少 POSIX 线程/pthread.h)
    • MCF(自 GCC 13 起,基于 MCF Gthread 库)
  • 异常模型:(看不懂,先抄下来)(对于C++实现而言的,坑爹的标准没有规定异常如何具体实现)
    • SjLj
    • SEH
    • Dwarf2
  • Windows上的运行时库:
    • MSVCRT:传统MinGW-w64 编译器选择的运行时库,在所有版本的 Windows 上可用
    • UCRT:从Windows10开始微软提供的新的运行时库,通常是更好的选择,除非windows版本过低

这些发行版的问题真是一言难尽,很可能出现:一段代码在上面的几个主流编译器中都可以编译通过,但是在MinGW-W64某个具体发行版就编译报错。不光编译,调试也是如此。 这一方面是因为 MinGW-W64 本身就是一个缝合怪,在两头顾及的过程中有很多困难,另一方面也是因为这些发行版的规模较小,没有经过充分的测试。而且并不能保证不同发行版的编译器构建输出之间的二进制兼容性。

如果可以的话,老老实实用前面的三种主流编译器环境就好,在windows上建议使用 WSL2,你永远不知道随意选择的某个MinGW-W64发行版在哪个坑等着你。

MSYS/MSYS2

还有一个很常见的名词——MSYS(minimal system),这个项目旨在为 Windows 系统提供一套类 UNIX shell 为基础的“系统”,它有时被视作 MinGW-W64 项目的一部分,但是实际上比其它发行版提供了更完整的“系统”模拟。

和作为原生 Win32 程序的 MinGW 不同,MSYS 环境下编译的本机程序依赖于额外的特定的 MSYS 运行时,更接近 Cygwin(强调 POSIX 兼容性),会有性能损失(但一般意义上比 Cygwin 轻量)。对应的三元组是 *-pc-msys。MSYS 提供了一个 sysroot 环境(下面有 /bin/etc 等),因此移植 POSIX 环境的程序一般更方便。

由于MSYS项目仅支持32位程序,并且项目本身发展缓慢,很快也迎来了类似MinGW-W64的重生: MSYS2基于最新版的Cygwin而创建,完全独立于MSYS而重写的版本,既支持32位程序又支持64位程序,POSIX兼容层为 msys-2.0.dll。比起 1.0 来说更加像 Cygwin。一个最大的特色是基础系统附带 ArchLinux 移植的包管理器 pacman 。

我完全没有使用过MSYS2,因此全部是参考的其他博客。常规的实践中,如果只是开发 Windows 程序,能用 MinGW 就不要用 MSYS 原生的编译器来构建。在WSL2出现之后MSYS已经被降维打击了,直接用WSL2绝对是更香的,而且WSL2直接受到微软官方支持和维护。

LLVM

前面的MinGW只是魔改gcc进行一些跨平台移植,LLVM的目标就宏大了许多:天下苦GNU GCC久矣,吾可取而代之。

背景故事

LLVM和Clang最初的编写者是一位叫做Chris Lattner的大神,硕博期间研究内容就是关于编译器优化的东西,发表了很多论文,博士论文是提出一套在编译时、链接时、运行时甚至是闲置时的优化策略,与此同时,LLVM的基本思想也就被确定了,这也让他在毕业前就在编译器圈子小有名气。

早期Apple公司一直使用gcc作为编译器,但是GCC对Apple主推的Objective-C的语言特性支持一直不够,Apple自己开发的GCC模块又很难得到GCC委员会的合并,所以有很多不满。等到Chris Lattner毕业时,Apple就把他招入靡下,去开发自己的编译器,所以LLVM最初(以及直到现在)受到了Apple的大力支持。

LLVM在最初设计时,因为只想着做优化方面的研究,所以只是想搭建一套虚拟机,所以当时这个的全称叫Low Level Virtual machine,后来目标变成了实现一整套编译器,于是官方就放弃了这个称呼,但是LLVM的简称还是保留下来了。

因为LLVM只是一个编译器框架,所以还需要一个前端来支撑整个系统,所以他们又一起研发了Clang,作为整个编译器的前端,用来编译C、C++和Objective-C。作为后来者,从各个角度来说,LLVM/Clang的优势都是GCC的巨量代码屎山所不能比的,但是因为Linux本身和GCC的深度绑定,两者同时发展了几十年,LLVM取而代之的路还很漫长。

LLVM Clang 一直受到 Apple 的大力支持,目前 macOS 所采用的编译器工具链全是LLVM的,甚至其中的gcc只是clang换了个名字。

GCC的问题

如同教科书上所说,静态语言的传统编译器在工作时的模型如下:

  1. 前端:负责解析源代码,检查语法错误,并将其翻译为抽象的语法树,得到中间代码。
  2. 优化器:对中间代码进行优化,试图使代码更高效。
  3. 后端:负责将优化器优化后的中间代码转换为目标机器的代码,最大化的利用目标机器的特殊指令,以提高代码的性能。

事实上,不光静态语言如此,动态语言也符合上面这个模型,例如 Java 的 JVM 也利用上面这个模型,将 Java 代码翻译为 Java bytecode。

这个三段式模型的好处是:

  • 当我们要支持多种语言时,只需要添加不同的前端进行替换就可以了;
  • 当需要支持多种目标机器时,只需要添加多个后端就可以了;
  • 对于中间的优化器,我们可以使用通用的中间代码。
  • 三段式模型可以大大降低开发难度,开发者只需要掌握单独一个环节的知识即可。

虽然教科书上的这种三段式编译器有很多优点,但是在实践中这一结构却从来没有被完美实现过。 GCC 在内部采用了三段式结构,并且分别实现了很多前端(例如 gfortran)来支持更多语言。 但是上述这些编译器的致命缺陷是,GCC 发布的是一个完整的可执行文件,而没有给其它语言的开发者提供代码重用的接口。即使 GCC 是开源的,但是巨量的屎山代码,使得源代码重用的难度非常大。例如,我们不可能将GCC嵌入到其他应用程序中、将GCC用作运行时/JIT编译器,或者在不引入大部分编译器的情况下提取和重用GCC的代码片段。

这是GCC的问题,也是LLVM这个后来者得到迅速发展的机会。

LLVM的结构

正如前面所说,GCC的问题就是结构不清晰,LLVM作为编译器框架,从一开始就具有清晰的模块化设计思想,例如可以将clang和lld都看做是LLVM的一个模块或者子项目。

LLVM实现了标准的三段式结构

LLVM包括的主要子项目如下:

  • LLVM Core libraries:LLVM 内核库提供了一套适合编译器系统的中间语言(Intermediate Representation,IR),在编译过程中有大量变换和优化都围绕其实现。经过变换和优化后的中间语言,可以转换为目标平台相关的汇编语言代码。
  • Clang:是一个基于 LLVM 架构的 C 语言家族(C / C ++ / Objective-C)编译器前端,其目地是提供一个快速编译的,非常有用的错误和警告消息,并为构建优秀的源代码级工具提供一个平台。(对标gcc,g++)
  • LLDB:是一个高性能的调试器。是 macOS 上 Xcode 中的默认调试器,并支持在台式机,iOS 设备和模拟器上调试 C,Objective-C 和 C ++。(对标gdb)
  • libc++ 和 libc++ ABI:项目提供了一个标准的符合性和高性能执行的 C++ 标准库。
  • ...

LLVM的模块化使它可以很容易和IDE集成,因为IDE软件可以直接调用库来实现一些如静态检查这些子功能。 开发者也很容易以此为基础构建生成各种功能的工具,因为新的工具只需要调用需要的库就行。 我们可以基于LLVM提供的功能开发自己的模块(还有自己的编程语言),并集成在LLVM系统上,增加它的功能,或者单纯自己开发软件工具,利用LLVM来支撑底层实现。

由于LLVM模块化的设计,取LLVM中的几个模块稍加修改,就可以很容易地在原本完整的GCC/MSVC工具链中替换某一个或者某一些环节,得到新的编译工具链,这使得编译工具链的选择变得非常多样,无比复杂!!!

Clang

Clang(Clang++)是LLVM框架下,用于编译c/c++的编译器前端,可以理解为对标gcc/g++,但实际上它并不能单独对抗GCC。 Clang作为后起之秀,它没有屎山代码需要处理,但是它也面临着兼容各种前辈的要求,Clang因此表现出了复杂的兼容性。

Clang既支持gcc风格的主要命令行参数(macOS甚至直接用clang来伪装gcc),也支持MSVC风格的主要命令行参数, 可以使用--driver-mode=选项切换,默认采用gcc风格的参数。

1
2
clang --driver-mode=gcc main.cpp -O2 -o output.exe
clang --driver-mode=cl main.cpp /EHsc /O2 /Feoutput.exe

LLVM Clang官方似乎并没有在Windows上大干一场的打算,目前在windows上使用clang有两个方向:

  • 一个方向就是兼容并替换MSVC(名字有时是clang-cl),这样就可以直接用MSVC的C++标准库,VS2022也提供了clang-cl可以选择下载。
  • 另一个方向是兼容并替换gcc,利用MinGW-W64提供的标准库,例如LLVM-MinGW

我们可以从clang的默认target查看两个方向的不同,例如下面这种输出代表它的默认使用MSVC标准库的版本

1
2
3
4
clang version 18.1.2
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: ...

而下面这种输出代表它是默认使用MinGW标准库的版本

1
2
3
4
clang version 18.1.2
Target: x86_64-pc-windows-gnu
Thread model: posix
InstalledDir: ...

作为参考,在Ubuntu系统中,clang会提供如下的输出

1
2
3
4
Ubuntu clang version 18.1.3
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

因为 MSVC 本身是不严格区分C/C++的,因此在x86_64-pc-windows-msvc目标下,clang和clang++其实没啥区别, 但是在x86_64-pc-windows-gnu目标下,clang和clang++是不一样的(正如gcc和g++的区别),clang编译cpp代码会报错,需要加上-lstdc++

这里讨论的只是版本信息中展示的默认target,clang其实可以加上-target选项来手动改变target,例如

1
clang++ -target x86_64-pc-windows-gnu main.cpp

此时clang就会试图在系统中查找MinGW的工具链来完成编译。

当前可以从LLVM的官网(或者官方仓库)下载的windows版本的clang等是不完整的,默认目标为x86_64-pc-windows-msvc,它假定windows平台上已经安装了MSVC或MinGW才能正常工作:

  • 如果windows上已经安装了MSVC,则clang作为前端加上MSVC的其它工具链和标准库,是可以顺利完成编译的;
  • 如果windows上已经安装了MinGW的某个发行版(发行版可以只含有gcc,不需要包括clang),并且添加到了PATH,那么clang在修改target之后,也是可以利用MinGW的工具链和标准库进行顺利编译的。

还需要注意的是在代码中如果需要判断当前编译器版本并进行不同处理,必须优先判断是不是clang,因为clang非常善于伪装自己是其他编译器(为了兼容性):在windows上可以伪装自己是MSVC,在Linux上可以伪装自己是gcc。

例如clang在windows上可能也会定义_MSC_VER这个宏(因为有的程序严重依赖这个宏),单独用这个宏并不能保证当前分支一定是MSVC编译器才进入,需要额外判断一下是否已经定义了__clang__这个clang自己的宏,最好的处理是先判断__clang__再去处理其他分支。

补充

写了这么多,记录一下当前的C/C++编译环境选择:(2024年4月15日)

  • Windows:
    • MSVC(VS2022
    • MinGW(gcc-13.2.0):选择winlibs发行版,目前选择了一个完全不含clang的版本;没有选择LLVM-MinGW
    • clang(clang-18.1.2):在LLVM仓库下载 clang+llvm-18.1.2-x86_64-pc-windows-msvc.tar.xz 或者 LLVM-18.1.2-win64.exe,这两个得到的内容其实差不多,但前者直接解压就行,后者是个安装程序,安装很慢。
  • WSL2(Ubuntu):
    • gcc(gcc-13.1.0
    • clang(clang-18.1.3

目前windows系统中存在MSVC和MinGW两个完整工具链,以及clang(默认目标为x86_64-pc-windows-msvc),可以切换clang的目标来使其分别使用MSVC或者MinGW的工具链和标准库。

这几个环境的编译器具体子版本不是完全一致的,因为各个平台的更新也不是完全同步的,但问题不大,保持gcc13clang18即可使用大部分C++20特性。