Visual Studio 2022 中的 C++ 一致性改进、行为更改和 bug 修复

Visual Studio 中的 Microsoft C++ (MSVC) 在每个版本中进行了符合性改进和 bug 修复。 本文按主要版本、然后是次要版本的形式列出了显著改进之处。 若要直接跳转到特定版本的更改,请使用“本文内容”链接。

本文档列出了 Visual Studio 2022 中的更改。

若要了解 Visual Studio 2019 中的更改,请参阅 Visual Studio 2019 中的 C++ 一致性改进
若要了解 Visual Studio 2017 中的更改,请参阅 Visual Studio 2017 中的 C++ 一致性改进
有关旧版本中的更改,请参阅 Visual C++ 新增功能 (2003 - 2015)

Visual Studio 2022 版本 17.9 中的符合性改进

Visual Studio 2022 版本 17.9 包含 Microsoft C/C++ 编译器中的以下符合性改进、bug 修复和行为变更。

有关对标准模板库所做的更改的更广泛摘要,请参阅 STL Changelog VS 2022 17.9

在 C 中对结构化类型应用 _Alignas

在 Visual Studio 2022 版本 17.9 之前的 Visual C++ 版本中,当 _Alignas 出现在声明中的结构类型旁边时,它未根据 ISO-C 标准正确应用。 例如:

// compile with /std:c17
#include <stddef.h>
struct Outer
{
    _Alignas(32) struct Inner { int i; } member1;
    struct Inner member2;
};
static_assert(offsetof(struct Outer, member2)==4, "incorrect alignment");

根据 ISO-C 标准,此代码应在不发出诊断 static_assert 的情况下进行编译。 _Alignas 指令仅适用于成员变量 member1。 它不得更改 struct Inner 的对齐方式。 但是,在 Visual Studio 版本 17.9.1 之前,发出了诊断“不正确的对齐方式”。 编译器将 member2struct Outer 中的 32 字节偏移量对齐。

修复此问题是二进制中断性变更,因此当应用此行为更改时,将发出警告。 对于前面的代码,警告 C5274“_Alignas 不再应用于类型“Inner”(仅适用于声明的数据对象)”现在在警告级别 1 发出。

在早期版本的 Visual Studio 中,_Alignas 在匿名类型声明旁边出现时,被忽略。 例如:

// compile with /std:c17
#include <stddef.h>
struct S {
    _Alignas(32) struct { int anon_member; };
    int k;
};
static_assert(offsetof(struct S, k)==4, "incorrect offsetof");
static_assert(sizeof(struct S)==32, "incorrect size");

以前,编译此代码时,这两个 static_assert 语句都失败。 代码现在编译,但出现以下级别 1 警告:

warning C5274: behavior change: _Alignas no longer applies to the type '<unnamed-tag>' (only applies to declared data objects)
warning C5273: behavior change: _Alignas on anonymous type no longer ignored (promoted members will align)

如果想要较早的行为,请将 _Alignas(N) 替换为 __declspec(align(N))。 与 _Alignas 不同,declspec(align) 可以应用于类型。

__VA_OPT__ 作为 /Zc:preprocessor 下的扩展启用

C++20 和 C23 中添加了 __VA_OPT__。 在添加之前,没有一种标准方法可以在可变参数宏中删除逗号。 为了提供更好的后向兼容性,我们在所有语言版本的基于令牌的预处理器 /Zc:preprocessor 下启用了 __VA_OPT__

例如,现在编译没有错误:

#define LOG_WRAPPER(message, ...) WRITE_LOG(__LINE__, message __VA_OPT__(, __VA_ARGS__))

// Failed to build under /std:c11, now succeeds.
LOG_WRAPPER("Log message");
LOG_WRAPPER("Log message with %s", "argument")

C23 语言

对于 C23,使用 /std:clatest 编译器开关时可以使用以下功能:

typeof
typeof_unqual

以下内容适用于所有 C 语言版本:

__typeof__
__typeof_unqual__

C++ 标准库

C++23 功能

  • formattablerange_formatformat_kindset_debug_format()P2286R8 格式设置范围的一部分
  • <mdspan>,遵循 P0009R18 以及应用于 C++23 Standard 的后续措辞更改。
  • format() 指针,遵循 P2510R3

Visual Studio 2022 版本 17.8 中的符合性改进

Visual Studio 2022 版本 17.8 包含 Microsoft C/C++ 编译器中的以下符合性改进、bug 修复和行为变更。

/FU 发出错误

C 编译器用于接受 /FU 选项,尽管该编译器已经有一段时间不支持托管编译了。 它现在会发出错误。 传递此选项的项目需要将其仅限于 C++/CLI 项目。

C++ 标准库

C++23 命名模块 stdstd.compat 现在可在编译 /std:c++20 时使用。

有关对 C++ 标准库所做的更改的更广泛摘要,请参阅 STL Changelog VS 2022 17.8

Visual Studio 2022 版本 17.7 中的符合性改进

Visual Studio 2022 版本 17.7 包含 Microsoft C/C++ 编译器中的以下符合性改进、bug 修复和行为变更。

向 C 编译器添加了 /std:clatest

此开关的行为类似于 C++ 编译器的 /std:c++latest 开关。 此开关启用了为下一个 C 标准草案提出的所有当前实现的编译器和标准库功能,以及一些正在进行和实验的功能。

C++ 标准库

现在支持 <print> 库。 请参阅 P2093R14 格式化输出

实现了 views::cartesian_product

有关对标准模板库所做的更改的更广泛摘要,请参阅 STL Changelog VS 2022 17.7

using 符合性

以前,using 指令可能会导致已用命名空间中的名称在不应显示时保持可见。 这可能会导致非限定名称查找在命名空间中查找名称,即使没有处于活动状态的 using 指令。

下面是新旧行为的一些示例。
以下注释中对“(1)”的引用意味着在命名空间 A 中调用 f<K>(t)

namespace A
{ 
    template<typename K, typename T> 
    auto f2(T t)
    { 
        return f<K>(t); // (1) Unqualified lookup should not find anything
    } 
} 

namespace B
{ 
    template<typename K, typename T> 
    auto f(T t) noexcept
    { // Previous behavior: This function was erroneously found during unqualified lookup at (1)
        return A::f2<K>(t); 
    } 
} 

namespace C
{ 
    template<typename T> 
    struct S {}; 

    template<typename, typename U> 
    U&& f(U&&) noexcept; // New behavior: ADL at (1) correctly finds this function 
} 

namespace D
{ 
    using namespace B; 

    void h()
    { 
        D::f<void>(C::S<int>()); 
    } 
} 

同样的基础问题可能会导致以前编译的代码现在被拒绝:

#include <memory>
namespace Addin {}
namespace Gui
{
    using namespace Addin;
}

namespace Addin
{
    using namespace std;
}

// This previously compiled, but now emits error C2065 for undeclared name 'allocator'.
// This should be declared as 'std::allocator<T*>' because the using directive nominating
// 'std' is not active at this point.
template <class T, class U = allocator<T*>>
class resource_list
{
};

namespace Gui
{
    typedef resource_list<int> intlist;
}

Visual Studio 2022 版本 17.6 中的合规性改进

Visual Studio 2022 版本 17.6 包含 Microsoft C/C++ 编译器的以下合规性改进、bug 修复和行为变更。

不再弃用复合 volatile 赋值

C++20 弃用了对使用 volatile 限定的类型应用特定运算符。 例如,使用 cl /std:c++20 /Wall test.cpp 编译以下代码时:

void f(volatile int& expr)
{
   ++expr;
}

编译器生成 test.cpp(3): warning C5214: applying '++' to an operand with a volatile qualified type is deprecated in C++20

C++20 中弃用了复合赋值运算符(@= 形式的运算符)。 C++23 中不再弃用 C++20 中排除的复合运算符。 例如,在 C++23 中,以下代码不会生成警告,但在 C++20 中会生成警告:

void f(volatile int& e1, int e2)
{
   e1 += e2;
}

有关这一更改的详细信息,请参阅 CWG:2654

重写表达式中的相等性算不上是中断性变更(P2468R2)

在 C++20 中,P2468R2 更改了编译器,使其接受类似于以下的代码:

struct S
{
    bool operator==(const S&);
    bool operator!=(const S&);
};
bool b = S{} != S{};

编译器接受此代码,这意味着编译器对类似于以下的代码更严格:

struct S
{
  operator bool() const;
  bool operator==(const S&);
};

bool b = S{} == S{};

编译器版本 17.5 接受此程序。 编译器版本 17.6 拒绝此程序。 若要解决此问题,请将 const 添加到 operator== 以删除歧义。 或将相应的 operator!= 添加到定义中,如下例所示:

struct S
{
  operator bool() const;
  bool operator==(const S&);
  bool operator!=(const S&);
};

bool b = S{} == S{};

Microsoft C/C++ 编译器版本 17.5 和 17.6 接受以前的程序,且在这两个版本中均调用 S::operator==

P2468R2 中概述的常规编程模型是,如果某个类型有对应的 operator!=,它通常会抑制重写行为。 对于以前在 C++17 中编译的代码,添加相应的 operator!= 是建议的修补方式。 有关详细信息,请参阅编程模型

Visual Studio 2022 版本 17.4 中的符合性改进

Visual Studio 2022 版本 17.4 包含 Microsoft C/C++ 编译器的以下符合性改进、bug 修复和行为变更。

无固定类型且未区分范围的 enum 的基础类型

在 Visual Studio 2022 版本 17.4 之前的 Visual Studio 版本中,C++ 编译器未正确确定无固定基类型的未区分范围的枚举的基础类型。 在 /Zc:enumTypes 下,我们现在正确地实现了标准行为。

C++ 标准要求 enum 的基础类型必须足够大,才能容纳该 enum 中的所有枚举器。 足够大的枚举器可以将 enum 的基础类型设置为 unsigned intlong longunsigned long long。 以前,这种 enum 类型在 Microsoft 编译器中始终具有基础类型 int,无需考虑枚举器的值。

启用 /Zc:enumTypes 选项后,它是一个潜在的源和二进制中断性变更。 默认情况下,它处于关闭状态,并且不会由 /permissive- 启用,因为该修复可能会影响二进制兼容性。 启用符合性修复后,一些枚举类型会改变大小。 一些 Windows SDK 标头包括此类枚举定义。

示例

enum Unsigned
{
    A = 0xFFFFFFFF // Value 'A' does not fit in 'int'.
};

// Previously, failed this static_assert. Now passes with /Zc:enumTypes.
static_assert(std::is_same_v<std::underlying_type_t<Unsigned>, unsigned int>);

template <typename T>
void f(T x)
{
}

int main()
{
    // Previously called f<int>, now calls f<unsigned int>.
    f(+A);
}

// Previously this enum would have an underlying type of `int`, but Standard C++ requires this to have
// a 64-bit underlying type. Using /Zc:enumTypes changes the size of this enum from 4 to 8, which could
// impact binary compatibility with code compiled with an earlier compiler version or without the switch.
enum Changed
{
    X = -1,
    Y = 0xFFFFFFFF
};

无固定基础类型的 enum 定义中的枚举器类型

在 Visual Studio 2022 版本 17.4 之前的 Visual Studio 版本中,C++ 编译器未正确地为枚举器类型建模。 如果在枚举的右大括号前面没有固定的基础类型,它可能会在枚举中假定一个不正确的类型。 在 /Zc:enumTypes 下,编译器现在正确地实现了标准行为。

C++ 标准指定,在没有固定基础类型的枚举定义中,由初始值设定项确定枚举器的类型。 或者,对于没有初始值设定项的枚举器,由上一个枚举器的类型确定(考虑到溢出)。 以前,此类枚举器始终被赋予枚举的推导类型,带基础类型的占位符(通常为 int)。

启用 /Zc:enumTypes 选项后,它是一个潜在的源和二进制中断性变更。 默认情况下,它处于关闭状态,并且不会由 /permissive- 启用,因为该修复可能会影响二进制兼容性。 启用符合性修复后,一些枚举类型会改变大小。 一些 Windows SDK 标头包括此类枚举定义。

示例

enum Enum {
    A = 'A',
    B = sizeof(A)
};

static_assert(B == 1); // previously failed, now succeeds under /Zc:enumTypes

在此示例中,枚举器 A 的类型 char 应位于枚举的右大括号之前,因此 B 应使用 sizeof(char) 进行初始化。 在 /Zc:enumTypes 修复之前,A 的枚举类型为 Enum,推导的基本类型为 intB 使用 sizeof(Enum) 进行了初始化,或者是 4。

Visual Studio 2022 版本 17.3 中的符合性改进

Visual Studio 2022 版本 17.3 包含 Microsoft C/C++ 编译器的以下符合性改进、bug 修复和行为变更。

C:改进了指针之间的修饰符兼容性检查

C 编译器无法在指针之间正确比较修饰符,尤其是 void*。 这一缺陷可能导致错误地诊断 const int**void* 之间的不兼容性以及 int* volatile*void* 之间的兼容性。

示例

void fn(void* pv) { (pv); }

int main()
{
    int t = 42;
    int* pt = &t;
    int* volatile * i = &pt;
    fn(i);    // Now raises C4090
    const int** j = &pt;
    fn(j);    // No longer raises C4090
}

Visual Studio 2022 版本 17.2 中的符合性改进

Visual Studio 2022 版本 17.2 包含 Microsoft C/C++ 编译器的以下符合性改进、bug 修复和行为变更。

未终止的双向字符警告

Visual Studio 2022 版本 17.2 为注释和字符串中未终止的 Unicode 双向字符添加了级别 3 警告 C5255。 该警告解决了特洛伊木马来源:不可见的漏洞(作者:Nicholas Boucher 和 Ross Anderson)中所述的安全问题。 有关 Unicode 双向字符的详细信息,请参阅 Unicode® 标准附录 9:UNICODE 双向算法

警告 C5255 仅处理转换后包含 Unicode 双向字符的文件。 此警告适用于 UTF-8、UTF-16 和 UTF-32 文件,因此必须提供正确的源编码。 这是一项源中断性变更。

示例(之前/之后)

在 Visual Studio 2022 版本 17.2 之前的 Visual Studio 版本中,未终止的双向字符未生成警告。 Visual Studio 2022 版本 17.2 生成警告 C5255:

// bidi.cpp
int main() {
    const char *access_level = "user";
    // The following source line contains bidirectional Unicode characters equivalent to:
    //    if ( strcmp(access_level, "user\u202e \u2066// Check if admin \u2069 \u2066") ) {
    // In most editors, it's rendered as:
    //    if ( strcmp(access_level, "user") ) { // Check if admin
    if ( strcmp(access_level, "user‮ ⁦// Check if admin ⁩ ⁦") ) {
        printf("You are an admin.\n");
    }
    return 0;
}

/* build output
bidi.cpp(8): warning C5255: unterminated bidirectional character encountered: 'U+202e'
bidi.cpp(8): warning C5255: unterminated bidirectional character encountered: 'U+2066'
*/

from_chars()float tiebreaker

Visual Studio 2022 版本 17.2 修复了生成错误结果的 <charconv>from_chars()float tiebreaker 规则中的一个 bug。 此 bug 影响了在一个狭窄范围内正好位于连续 float 值中点的十进制字符串。 (受影响的最小值和最大值分别 32768.009765625131071.98828125。)tiebreaker 规则希望舍入到“偶数”,“偶数”刚好是“向下”舍入,但实现却错误地“向上”舍入。(double 未受影响。)有关详细信息和实现详情,请参阅 microsoft/STL#2366

此更改会影响指定情况范围内的运行时行为:

示例

// from_chars_float.cpp
#include <cassert>
#include <charconv>
#include <cstdio>
#include <string_view>
#include <system_error>
using namespace std;
int main() {
    const double dbl  = 32768.009765625;
    const auto sv     = "32768.009765625"sv;
    float flt         = 0.0f;
    const auto result = from_chars(sv.data(), sv.data() + sv.size(), flt);
    assert(result.ec == errc{});
    printf("from_chars() returned: %.1000g\n", flt);
    printf("This rounded %s.\n", flt < dbl ? "DOWN" : "UP");
}

在 Visual Studio 2022 版本 17.2 之前的版本中:

C:\Temp>cl /EHsc /nologo /W4 /std:c++17 from_chars_float.cpp && from_chars_float
from_chars_float.cpp
from_chars() returned: 32768.01171875
This rounded UP.

在 Visual Studio 2022 版本 17.2 及后续版本中:

C:\Temp>cl /EHsc /nologo /W4 /std:c++17 from_chars_float.cpp && from_chars_float
from_chars_float.cpp
from_chars() returned: 32768.0078125
This rounded DOWN.

/Zc:__STDC__ 使 __STDC__ 可用于 C

C 标准要求符合的 C 实现将 __STDC__ 定义为 1。 由于 UCRT 的行为(在 __STDC__1 时不公开 POSIX 函数),所以在不对稳定语言版本引入中断性变更的情况下,不可能默认为 C 定义此宏。 Visual Studio 2022 版本 17.2 及更高版本添加了一个一致性选项 /Zc:__STDC__,用于定义此宏。 该选项没有更早的版本。 目前,我们计划在将来的 C 版本中默认使用此选项。

这是一项源中断性变更。 它在启用了 C11 或 C17 模式时适用,/std:c11/std:c17,与 /Zc:__STDC__ 一起应用。

示例

// test__STDC__.c
#include <io.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
#if __STDC__
    int f = _open("file.txt", _O_RDONLY);
    _close(f);
#else
    int f = open("file.txt", O_RDONLY);
    close(f);
#endif
}

/* Command line behavior

C:\Temp>cl /EHsc /W4 /Zc:__STDC__ test__STDC__.c && test__STDC__

*/

缺少大括号的警告

警告 C5246 报告子对象聚合初始化期间缺少大括号。 在 Visual Studio 2022 版本 17.2 之前的版本中,该警告未处理匿名 structunion 的情况。

这是一项源中断性变更。 它在启用默认警告 C5246 时适用。

示例

在 Visual Studio 2022 版本 17.2 及更高版本中,此代码现在会导致错误:

struct S {
   union {
      float f[4];
      double d[2];
   };
};

void f()
{
   S s = { 1.0f, 2.0f, 3.14f, 4.0f };
}

/* Command line behavior
cl /Wall /c t.cpp

t.cpp(10): warning C5246: 'anonymous struct or union': the initialization of a subobject should be wrapped in braces
*/

若要解决此问题,请向初始化表达式添加大括号:

void f()
{
   S s = { { 1.0f, 2.0f, 3.14f, 4.0f } };
}

Visual Studio 2022 版本 17.1 中的符合性改进

Visual Studio 2022 版本 17.1 包含 Microsoft C/C++ 编译器的以下符合性改进、bug 修复和行为变更。

在非局部 lambda 表达式中检测格式错误的捕获默认值

C++ 标准仅允许块范围中的 Lambda 表达式具有捕获默认值。 在 Visual Studio 2022 版本 17.1 及更高版本中,编译器会检测非本地 Lambda 表达式中不允许使用捕获默认值的情况。 它会发出新的级别 4 警告 C5253。

这是一项源中断性变更。 它适用于任何使用新的 Lambda 处理器的模式:/Zc:lambda/std:c++20/std:c++latest

示例

在 Visual Studio 2022 版本 17.1 中,此代码现在会发出错误:

#pragma warning(error:5253)

auto incr = [=](int value) { return value + 1; };

// capture_default.cpp(3,14): error C5253: a nonlocal lambda cannot have a capture default
// auto incr = [=](int value) { return value + 1; };
//              ^

要解决此问题,请删除捕获默认值:

#pragma warning(error:5253)

auto incr = [](int value) { return value + 1; };

C4028 现在为 C4133,用于函数到指针的操作

在 Visual Studio 2022 版本 17.1 之前,编译器报告了 C 代码中有关某些指针到函数比较的一个错误消息。 当你比较两个具有相同参数计数但类型不兼容的函数指针时,报告了不正确的消息。 现在,我们发出不同的警告,投诉指针到函数的不兼容性,而不是函数参数不匹配。

这是一项源中断性变更。 它适用于代码被编译为 C 的情况。

示例

int f1(int); 
int f2(char*); 
int main(void) 
{ 
    return (f1 == f2); 
}
// Old warning:
// C4028: formal parameter 1 different from declaration
// New warning:
// C4113: 'int (__cdecl *)(char *)' differs in parameter lists from 'int (__cdecl *)(int)'

有关独立 static_assert 的错误

在 Visual Studio 2022 版本 17.1 及更高版本中,如果与 static_assert 关联的表达式不是依赖性表达式,则编译器会在分析表达式时对其进行计算。 如果表达式的计算结果为 false,编译器将发出错误。 以前,如果 static_assert 在一个函数模板的主体内(或在一个类模板的成员函数的主体内),编译器就不会执行这种分析。

这是一项源中断性变更。 它适用于任何表示 /permissive-/Zc:static_assert 的模式。 可以使用 /Zc:static_assert- 编译器选项来禁用此行为更改。

示例

在 Visual Studio 2022 版本 17.1 及更高版本中,此代码现在会导致错误:

template<typename T>
void f()
{
   static_assert(false, "BOOM!");
}

要解决此问题,需要使表达式具有依赖性。 例如:

template<typename>
constexpr bool dependent_false = false;

template<typename T>
void f()
{
   static_assert(dependent_false<T>, "BOOM!");
}

进行此更改后,编译器仅在函数模板 f 经过实例化后发出错误。

Visual Studio 2022 版本 17.0 中的符合性改进

Visual Studio 2022 版本 17.0 包含 Microsoft C/C++ 编译器的以下符合性改进、bug 修复和行为变更。

关于枚举类型位域宽度的警告

将枚举类型的实例声明为位域时,位域的宽度必须容纳枚举的所有可能值。 否则,编译器将发出诊断消息。 请考虑以下示例:

enum class E : unsigned { Zero, One, Two };

struct S {
  E e : 1;
};

程序员可能希望类成员 S::e 能够容纳所有显式命名的 enum 值。 鉴于枚举元素的数量,这是不可能的。 位域无法涵盖显式提供的 E 值的范围(从概念上来讲,这是 E 的域)。 为了解决位域宽度不够大而无法容纳美爵的域这一问题,向 MSVC 添加了一个新的警告(它默认关闭):

t.cpp(4,5): warning C5249: 'S::e' of type 'E' has named enumerators with values that cannot be represented in the given bit field width of '1'.
  E e : 1;
    ^
t.cpp(1,38): note: see enumerator 'E::Two' with value '2'
enum class E : unsigned { Zero, One, Two };
                                     ^

此编译器行为是一项源和二进制中断性变更,会影响所有 /std/permissive 模式。

针对 nullptr 或 0 的有序指针比较出错

C++ 标准无意中允许了针对 nullptr 或 0 的有序指针比较。 例如:

bool f(int *p)
{
   return p >= 0;
}

WG21 规定 N3478 删除了这一疏忽。 此更改已在 MSVC 中实现。 使用 /permissive-(和 /diagnostics:caret)编译示例时,会出现以下错误:

t.cpp(3,14): error C7664: '>=': ordered comparison of pointer and integer zero ('int *' and 'int')
    return p >= 0;
             ^

此编译器行为是一项源和二进制中断性变更,会影响所有 /std 模式中使用 /permissive- 编译的代码。

另请参阅

Microsoft C/C++ 语言一致性