分享方式:


Visual Studio 2019 中的 C++ 一致性改善、行為變更和錯誤修正

Visual Studio 中的 Microsoft C/C++ (MSVC) 在每個版本中都進行一致性改善和錯誤 (bug) 修正。 本文依主要版次和版本的順序列出列出改善。 若要直接跳到特定版本的變更,請使用本文內容下方的清單。

本文件列出 Visual Studio 2019 中的變更。 如需 Visual Studio 2022 中變更的指南,請參閱 Visual Studio 2022 中的 C++ 一致性改善。 針對 Visual Studio 2017 中的變更,請參閱 Visual Studio 2017 中的 C++ 一致性改善。 如需先前一致性改善的完整清單,請參閱從 2003 到 2015 的 Visual C++ 新功能 (部分機器翻譯)。

Visual Studio 2019 RTW (16.0 版) 中的一致性改善

Visual Studio 2019 RTW 包含下列一致性改善、Bug 修正和 Microsoft C++ 編譯器的行為變更。

注意

C++20 功能只能在 Visual Studio 2019 的 /std:c++latest 模式中使用,直到 C++20 實作被視為完成為止。 Visual Studio 2019 16.11 版引進了 /std:c++20 編譯器模式。 在本文中,原本需要 /std:c++latest 模式的功能現在可在最新版 Visual Studio 的 /std:c++20 模式或更新版本中運作。 我們已更新文件以提及 /std:c++20,即使此選項在功能首次發行時並無法使用。

改善的範本和錯誤偵測模組支援

模組現已正式使用 C++20 標準。 Visual Studio 2017 15.9 版已新增改善的支援。 如需詳細資訊,請參閱 Better template support and error detection in C++ Modules with MSVC 2017 version 15.9 (C++ 模組與 MSVC 2017 15.9 版的更佳範本支援和錯誤偵測)。

已修改彙總類型的規格

C++20 已變更彙總類型的規格 (請參閱 Prohibit aggregates with user-declared constructors (禁止使用使用者宣告的建構函式彙總))。 在 Visual Studio 2019 中,於 /std:c++latest 下 (針對 Visual Studio 2019 16.11 版或更新版本則是在 /std:c++20 下),任何具有使用者宣告建構函式的類別 (例如,包括建構函式宣告的 = default= delete) 都不是彙總。 過去,只有使用者所提供建構函式才可取消讓類別成為彙總的資格。 此變更會對將此類類型初始化的方式加諸更多限制。

在 Visual Studio 2017 中,下列程式碼在編譯時不會引發錯誤,但在 Visual Studio 2019 的 /std:c++20/std:c++latest 下會引發 C2280 和 C2440 錯誤:

struct A
{
    A() = delete; // user-declared ctor
};

struct B
{
    B() = default; // user-declared ctor
    int i = 0;
};

A a{}; // ill-formed in C++20, previously well-formed
B b = { 1 }; // ill-formed in C++20, previously well-formed

部分支援 operator <=>

P0515R3 C++20 引入 <=> 三向比較運算子,也稱為「太空船運算子」。 處於 /std:c++latest 模式的 Visual Studio 2019 16.0 版會透過引發目前不允許的語法錯誤,引入運算子部分支援。 例如,在 Visual Studio 2017 中,下列程式碼在編譯時不會引發錯誤,但在 Visual Studio 2019 的 /std:c++20/std:c++latest 下會引發多個錯誤:

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

template <bool (S::*)(const S&) const>
struct U { };

int main(int argc, char** argv)
{
    U<&S::operator<=> u; // In Visual Studio 2019 raises C2039, 2065, 2146.
}

為避免發生錯誤,請在有問題程式行的最後一個角括弧前插入空格:U<&S::operator<= > u;

參考具有不相符 cv 限定詞的類型

注意

此變更只會影響 Visual Studio 2019 16.0 到 16.8 版。 其從 Visual Studio 2019 16.9 版開始還原

在過去,MSVC 允許直接從最高層級底下具有不相符 CV 限定詞的類型繫結參考。 此繫節可能允許修改參考應參考的 const 資料。

Visual Studio 2019 16.0 到 16.8 版的編譯器會改為建立暫存,如同當時的標準所要求。 之後,標準會追朔性地變更,使 Visual Studio 2017 和更早版本的行為變成是正確的,而 Visual Studio 2019 16.0 版至 16.8 版的行為變成是錯誤的。 因此,此變更已於 Visual Studio 2019 16.9 版開始還原。

如需相關變更,請參閱類似類型和參考繫結

例如,在 Visual Studio 2017 中,下列程式碼在編譯時不會發出警告。 在 Visual Studio 2019 16.0 到 16.8 版中,編譯器會引發警告 C4172。 從 Visual Studio 2019 16.9 版開始,程式碼再次能在不發出警告的情況下進行編譯:

struct X
{
    const void* const& PData() const
    {
        return _pv;
    }

    void* _pv;
};

int main()
{
    X x;
    auto p = x.PData(); // C4172 <func:#1 "?PData@X@@QBEABQBXXZ"> returning address of local variable or temporary
}

來自多載函式的 reinterpret_cast

reinterpret_cast 的引數不是允許多載函式位址的內容之一。 在 Visual Studio 2017 中,下列程式碼在編譯時不會引發錯誤,但在 Visual Studio 2019 中會引發錯誤 C2440:

int f(int) { return 1; }
int f(float) { return .1f; }
using fp = int(*)(int);

int main()
{
    fp r = reinterpret_cast<fp>(&f); // C2440: cannot convert from 'overloaded-function' to 'fp'
}

為避免此錯誤,此案例請使用允許的轉換:

int f(int);
int f(float);
using fp = int(*)(int);

int main()
{
    fp r = static_cast<fp>(&f); // or just &f;
}

Lambda 終止

在 C++14 中,Lambda 終止類型不是常值。 此規則的主要的結果是 Lambda 可能不會指派給 constexpr 變數。 在 Visual Studio 2017 中,下列程式碼在編譯時不會引發錯誤,但在 Visual Studio 2019 中會引發錯誤 C2127:

int main()
{
    constexpr auto l = [] {}; // C2127 'l': illegal initialization of 'constexpr' entity with a non-constant expression
}

為避免此錯誤,請移除 constexpr 限定詞,或將一致性模式變更為 /std:c++17 或更新版本。

std::create_directory 失敗碼

已從 C++20 無條件實作 P1164。 這會變更 std::create_directory,檢查目標在發生故障時是否即為目錄。 以前,所有 ERROR_ALREADY_EXISTS 類型錯誤都會轉換成成功但未建立目錄的程式碼。

operator<<(std::ostream, nullptr_t)

根據 LWG 2221 (英文),已新增 operator<<(std::ostream, nullptr_t) 以將 nullptr 寫入資料流。

更多平行演算法

新的平行版 is_sortedis_sorted_untilis_partitionedset_differenceset_intersectionis_heapis_heap_until

不可部分完成的初始化中的修正

P0883「修正不可部分完成的初始化」std::atomic 變更為初始化包含 T 的值,而不是將其初始化的預設值。 使用 Clang/LLVM 與 Microsoft 標準程式庫時即啟用此修正。 目前因使用 Microsoft C++ 編譯器作為 constexpr 處理的 Bug 因應措施而停用。

remove_cvrefremove_cvref_t

已從 P0550 實作 remove_cvrefremove_cvref_t 類型特性。 它們會移除類型中的參考性質和 CV 限定性,但不衰減指標的函式和陣列 (不同於 std::decaystd::decay_t)。

功能測試巨集

P0941R2 - 功能測試巨集已完成,並支援 __has_cpp_attribute。 所有標準模式都支援功能測試巨集。

禁止使用使用者宣告的建構函式彙總

C++20 P1008R1 - 禁止使用使用者宣告的建構函式彙總 (英文) 已完成。

constexpr 函式中的 reinterpret_cast

constexpr 函式中的 reinterpret_cast 是不合法的。 Microsoft C++編譯器先前只會在 reinterpret_cast 是用於 constexpr 內容中時才加以拒絕。 在 Visual Studio 2019 的所有語言標準模式中,編譯器會正確診斷 constexpr 函式定義中的 reinterpret_cast。 下列程式碼現在會產生 C3615:

long long i = 0;
constexpr void f() {
    int* a = reinterpret_cast<int*>(i); // C3615: constexpr function 'f' cannot result in a constant expression
}

若要避免錯誤,請從函式宣告中移除 constexpr 修飾元。

basic_string 範圍建構函式的正確診斷

在 Visual Studio 2019 中,basic_string 範圍建構函式不再使用 static_cast 隱藏編譯器診斷。 在 Visual Studio 2017 中,下列程式碼在編譯時不會發出警告,但在初始化 out 時,從 wchar_tchar 可能會遺失資料:

std::wstring ws = /* . . . */;
std::string out(ws.begin(), ws.end()); // VS2019 C4244: 'argument': conversion from 'wchar_t' to 'const _Elem', possible loss of data.

Visual Studio 2019 會正確引發警告 C4244。 若要避免此警告,您可以將 std::string 初始化,如此範例所示:

std::wstring ws = L"Hello world";
std::string out;
for (wchar_t ch : ws)
{
    out.push_back(static_cast<char>(ch));
}

現可正確偵測 /clr/ZW 下的對 +=-= 的不正確呼叫

Visual Studio 2017 中引入的 Bug 導致編譯器以無訊息方式忽略錯誤,且不會為 /clr/ZW 下的 +=-= 無效呼叫產生程式碼。 在 Visual Studio 2017 中,下列程式碼在編譯時不會引發錯誤,但在 Visual Studio 2019 中會正確引發「錯誤 C2845:

public enum class E { e };

void f(System::String ^s)
{
    s += E::e; // in VS2019 C2845: 'System::String ^': pointer arithmetic not allowed on this type.
}

若要避免此範例中的錯誤,請使用 += 運算子搭配 ToString() 方法:s += E::e.ToString();

內嵌靜態資料成員的初始設定式

現可正確偵測到 inlinestatic constexpr 初始設定式內的無效成員存取。 下列範例會在沒有錯誤的情況下於 Visual Studio 2017 中進行編譯,但在 /std:c++17 模式的 Visual Studio 2019 或更新版本中會引發錯誤 C2248:

struct X
{
    private:
        static inline const int c = 1000;
};

struct Y : X
{
    static inline int d = c; // VS2019 C2248: cannot access private member declared in class 'X'.
};

為避免此錯誤,請將成員 X::c 宣告為受保護:

struct X
{
    protected:
        static inline const int c = 1000;
};

已重新啟用 C4800

MSVC 之前會發出有關隱含轉換為 bool 的效能警告 C4800。 其太頻繁地發出且無法隱藏,因此我們決定在 Visual Studio 2017 中將其移除。 不過,Visual Studio 2017 在生命週期中獲得許多有助於解決案例問題的意見反應。 我們在 Visual Studio 2019 中重新加入,仔細設計的 C4800,以及說明性的 C4165。 這兩個警告都很容易隱藏:無論是使用明確轉換,還是透過與 0 個適當的類型進行比較。 C4800 為預設關閉 (部分機器翻譯) 的層級 4 警告,且 C4165 是預設關閉的層級 3 警告。 兩者都可使用 /Wall 編譯器選項探索。

下列範例在 /Wall 下會引發 C4800 和 C4165:

bool test(IUnknown* p)
{
    bool valid = p; // warning C4800: Implicit conversion from 'IUnknown*' to bool. Possible information loss
    IDispatch* d = nullptr;
    HRESULT hr = p->QueryInterface(__uuidof(IDispatch), reinterpret_cast<void**>(&d));
    return hr; // warning C4165: 'HRESULT' is being converted to 'bool'; are you sure this is what you want?
}

為避免上例中的警告,您可以撰寫如下的程式碼:

bool test(IUnknown* p)
{
    bool valid = p != nullptr; // OK
    IDispatch* d = nullptr;
    HRESULT hr = p->QueryInterface(__uuidof(IDispatch), reinterpret_cast<void**>(&d));
    return SUCCEEDED(hr);  // OK
}

區域類別成員函式沒有主體

在 Visual Studio 2017 中,只有在明確設定編譯器選項 /w14822 時,才會引發警告 C4822。 其不會與 /Wall 一起顯示。 在 Visual Studio 2019 中,C4822 為預設關閉 (部分機器翻譯) 的警告,因此不必明確設定 /w14822,即可在 /Wall 下加以探索。

void example()
{
    struct A
        {
            int boo(); // warning C4822: Local class member function doesn't have a body
        };
}

包含 if constexpr 陳述式的函式範本主體

在處於 /std:c++20/std:c++latest 的 Visual Studio 2019 中,具有 if constexpr 陳述式的範本函式主體已啟用額外的剖析相關檢查。 例如,在 Visual Studio 2017 中,只有在設定 /permissive- 選項時,下列程式碼才會產生 C7510 (部分機器翻譯)。 在 Visual Studio 2019 中,即使設定 /permissive 選項,相同的程式碼還是會引發錯誤:

// C7510.cpp
// compile using: cl /EHsc /W4 /permissive /std:c++latest C7510.cpp
#include <iostream>

template <typename T>
int f()
{
    T::Type a; // error C7510: 'Type': use of dependent type name must be prefixed with 'typename'
    // To fix the error, add the 'typename' keyword. Use this declaration instead:
    // typename T::Type a;

    if constexpr (a.val)
    {
        return 1;
    }
    else
    {
        return 2;
    }
}

struct X
{
    using Type = X;
    constexpr static int val = 1;
};

int main()
{
    std::cout << f<X>() << "\n";
}

為避免此錯誤,請將 typename 關鍵字新增至 a 宣告:typename T::Type a;

Lambda 運算式不支援內嵌組譯碼

Microsoft C++ 小組最近發現一個安全性問題,即在 Lambda 內使用內嵌組合語言可能會導致 ebp (傳回位登錄) 在執行階段損毀。 惡意攻擊者可能利用這種情況。 只有在 x86 上才支援內嵌組合語言,且內嵌組合語言與編譯器其餘部分之間的互動性很差。 鑒於這些事實和問題的性質,此問題最安全的解決方案是不允許在 Lambda 運算式內使用內嵌組合語言。

我們所見唯一會在 Lambda 運算式內使用內嵌組譯工具的實際情況,是擷取傳回位址。 在此案例中,您可以擷取所有平台的寄件地址,只要使用編譯器內建 _ReturnAddress() 即可。

下列程式碼會在 Visual Studio 2017 15.9 和更新版本的 Visual Studio 中產生 C7553 (部分機器翻譯):

#include <cstdio>

int f()
{
    int y = 1724;
    int x = 0xdeadbeef;

    auto lambda = [&]
    {
        __asm {  // C7553: inline assembler is not supported in a lambda

            mov eax, x
            mov y, eax
        }
    };

    lambda();
    return y;
}

為避免此錯誤,請將組譯碼移入具名函式,如下列範例所示:

#include <cstdio>

void g(int& x, int& y)
{
    __asm {
        mov eax, x
        mov y, eax
    }
}

int f()
{
    int y = 1724;
    int x = 0xdeadbeef;
    auto lambda = [&]
    {
        g(x, y);
    };
    lambda();
    return y;
}

int main()
{
    std::printf("%d\n", f());
}

迭代器偵錯和 std::move_iterator

已教授迭代器偵錯功能正確解除包裝 std::move_iterator。 例如,std::copy(std::move_iterator<std::vector<int>::iterator>, std::move_iterator<std::vector<int>::iterator>, int*) 現可投入 memcpy 快速路徑。

適用於 <xkeycheck.h> 關鍵字強制的修正

已修正 <xkeycheck.h> 中適用於取代關鍵字之巨集的標準程式庫強制。 程式庫現在會發出偵測到的實際問題關鍵字,而不是泛型訊息。 它也支援 C++20 的關鍵字,可避免誘騙 IntelliSense 說出隨機的巨集關鍵字。

配置器類型不再予以淘汰

std::allocator<void>std::allocator::size_typestd::allocator::difference_type 不再予以淘汰。

更正縮小字串轉換的警告

std::string 中移除標準未呼叫且意外隱藏 C4244 縮小警告的假性 static_cast。 嘗試呼叫 std::string::string(const wchar_t*, const wchar_t*) 現在會正確地發出 C4244,以將 wchar_t 縮小為 char

針對 <filesystem> 正確性的各種修正

  • 已修正 std::filesystem::last_write_time 在嘗試變更目錄的上次寫入時間時發生失敗的問題。
  • 提供不存在的目標路徑時,std::filesystem::directory_entry 建構函式現在會儲存失敗的結果,而不是擲回例外狀況。
  • std::filesystem::create_directory 的 2 個參數版本已變更為呼叫 1 個參數版本,因為當 existing_p 為符號連結時,基礎 CreateDirectoryExW 函式會使用 copy_symlink
  • 當發現符號連結中斷時,std::filesystem::directory_iterator 不會再失敗。
  • std::filesystem::space 現在接受相對路徑。
  • std::filesystem::path::lexically_relative 不再為句尾斜線困惑,回報為 LWG 3096
  • 因應措施為 CreateSymbolicLinkW 拒絕在 std::filesystem::create_symlink 中有正斜線的路徑。
  • 提供 Windows 10 LTSB 1609 中存在 POSIX 刪除模式 delete 函式,但其無法實際刪除檔案的因應措施。
  • std::boyer_moore_searcherstd::boyer_moore_horspool_searcher 的複製建構函式和複製指派運算子現都可實際複製項目。

Windows 8 和更新版本中的平行演算法

平行演算法程式庫現可正確使用 Windows 8 和更新版本的真實 WaitOnAddress 系列,而不是一律使用 Windows 7 和舊版的假版本。

std::system_category::message() 空白字元

std::system_category::message() 現在會修剪傳回訊息的尾端空格。

std::linear_congruential_engine 除以零

已修正會導致 std::linear_congruential_engine 觸發除以 0 的一些情況。

修正迭代器解除包裝

某些迭代器解除包裝機制首次在 Visual Studio 2017 15.8 中針對程式設計師使用者整合公開。 其已在 C++ 小組部落格文章 VS 2017 15.8 中的 STL 功能和修正 (英文) 中說明。 此機制不會再將衍生自標準程式庫迭代器的迭代器解除包裝。 例如,衍生自 std::vector<int>::iterator 的使用者和嘗試自訂行為的使用者,現在都可在呼叫標準程式庫演算法時試取得其自訂的行為,而不是指標的行為。

未排序的容器 reserve 函式現在實際上都會保留以供 N 個元素使用,如 LWG 2156 \(英文\) 中所述。

時間處理

  • 過去,某些已傳遞給並行程式庫的時間值會溢位,例如 condition_variable::wait_for(seconds::max())。 現已修正,溢位過去似乎會以隨機的 29 日循環變更行為 (當基礎 Win32 API 接受的 uint32_t 毫秒溢位時)。

  • <ctime> 標頭現在會正確地宣告命名空間 std 中的 timespectimespec_get,並在全域命名空間中加以宣告。

容器的各種修正

  • 許多標準程式庫的內部容器函式都已成為 private,以改善 IntelliSense 體驗。 MSVC 的更新版本預期會有更多將成員標示為 private 的修正。

  • 我們已修正例外狀況安全正確性問題,其導致節點型容器 (例如 listmapunordered_map) 損毀。 在 propagate_on_container_copy_assignmentpropagate_on_container_move_assignment 重新指派作業期間,我們會利用舊配置器來釋放容器的 sentinel 節點、對舊的配置器執行 POCCA/POCMA 指派,然後重試取得新配置器的 sentinel 節點。 如果此配置失敗,便代表容器已損毀。 其甚至無法終結,因為擁有 sentinel 節點是不區分硬資料結構的。 此程式碼已修正,以透過在終結現有 sentinel 節點之前使用來源容器的配置器來建立新的 sentinel 節點。

  • 容器已修正為根據 propagate_on_container_copy_assignmentpropagate_on_container_move_assignmentpropagate_on_container_swap,甚至針對配置器宣告的 is_always_equal 來一律複製/移動/分頁配置器。

  • 已新增接受右值容器的容器合併和擷取成員函式的多載。 如需詳細資訊,請參閱 P0083「接合對應與集合」

std::basic_istream::read\r\n 處理 =>\n

std::basic_istream::read 已修正為不寫入部分所提供緩衝區轉存作為 \r\n\n 處理的一部分。 此變更放棄 Visual Studio 2017 15.8 中大小大於 4K 之讀取的效能優勢。 不過,避免每個字元三次虛擬呼叫的效率改善仍存在。

std::bitset 建構函式

std::bitset 建構函式不會再反向讀取大型位元集的一和零。

std::pair::operator= 迴歸

已修正實作 LWG 2729「std::pair::operator= 上遺漏 SFINAE」 (英文) 時,std::pair 指派運算子導入的迴歸。 它現在可以再次正確接受可轉換為 std::pair 的類型。

add_const_t 的非推算內容

已修正 add_const_t 和相關函式本該是非推算內容的次要類型特徵 Bug。 換言之,add_const_t 應該是 typename add_const<T>::type 的別名,而非 const T 的別名。

16.1 中的一致性改善

char8_t

P0482r6。 C++20 新增用來表示 UTF-8 字碼單位的新字元類型。 C++20 的 u8 字串常值具有類型 const char8_t[N] 而非 const char[N],這是舊例。 N2231 \(英文\) 已針對 C 標準建議類似的變更。 P1423r3 (英文) 提供 char8_t 回溯相容性的補救建議。 Microsoft C++ 編譯器新增在您指定 /Zc:char8_t (部分機器翻譯) 編譯器選項時針對 Visual Studio 2019 16.1 版中 char8_t 的支援。 其可透過 /Zc:char8_t- 還原為 C++17 行為。 支援 IntelliSense 的 EDG 編譯器尚不支援 Visual Studio 2019 16.1 版。 您可能會看到不影響實際編譯、僅限 IntelliSense 的假性錯誤。

範例

const char* s = u8"Hello"; // C++17
const char8_t* s = u8"Hello"; // C++20

std::type_identity 中繼函式和 std::identity 函式物件

P0887R1 type_identity. 已移除淘汰的 std::identity 類別範本副檔名,並已經以 C++20 std::type_identity metafunction 和 std::identity 函式物件取代。 這兩者僅於 /std:c++latest (在 Visual Studio 2019 16.11 版和更新版本中則為 /std:c++20) 底下提供。

下列範例會在 Visual Studio 2017 中針對 std::identity 產生淘汰警告 C4996 (定義於 <type_traits>):

#include <type_traits>

using T = std::identity<int>::type;
T x, y = std::identity<T>{}(x);
int i = 42;
long j = std::identity<long>{}(i);

下列範例示範如何搭配使用新的 std::identity (定義於 <functionalstd::type_identity) 以及新的 >:

#include <type_traits>
#include <functional>

using T = std::type_identity<int>::type;
T x, y = std::identity{}(x);
int i = 42;
long j = static_cast<long>(i);

泛型 Lambda 的語法檢查

新的 Lambda 處理器可在 /std:c++latest 底下 (在 Visual Studio 2019 16.11 版或更新版本中則為 /std:c++20),或針對 Visual Studio 2019 16.9 版或更新版本搭配 /Zc:lambda (部分機器翻譯) (先前從 Visual Studio 2019 16.3 版開始是以 /experimental:newLambdaProcessor 的形式提供) 在任何其他語言模式底下,針對泛型 Lambda 提供一些一致性模式語法檢查。

舊版 Lambda 處理器會在無警告的情況下編譯此範例,但新的 Lambda 處理器會產生錯誤 C2760:

void f() {
    auto a = [](auto arg) {
        decltype(arg)::Type t; // C2760 syntax error: unexpected token 'identifier', expected ';'
    };
}

此範例會示範正確的語法,其現由編譯器強制執行:

void f() {
    auto a = [](auto arg) {
        typename decltype(arg)::Type t;
    };
}

函式呼叫的引數相依查閱

P0846R0 (英文) (C++20) 為具有明確範本引數的函式呼叫運算式,增強透過依引數相關查閱來尋找函式範本的功能。 需要 /std:c++latest (在 Visual Studio 2019 16.11 和更新版本中則為 /std:c++20)。

指定的初始化

P0329R4 (英文) (C++20) 指定的初始化使用 Type t { .member = expr } 語法,在彙總初始化中選取特定成員。 需要 /std:c++latest (在 Visual Studio 2019 16.11 和更新版本中則為 /std:c++20)。

列舉轉換成其固定底層類型的排名

編譯器現在會根據 N4800 (英文) 11.3.3.2 排名隱含轉換序列 (4.2) 對列舉轉換進行排名:

  • 比起在類型不同的情況下提升至所提升底層類型的轉換,將底層類型為固定的列舉提升為其底層類型的轉換會比較好。

此轉換排名在 Visual Studio 2019 16.1 版之前並未正確實作。 一致性行為可能會變更多載解析行為或公開先前未偵測到其中一個的模玲兩可情況。

此編譯器行為變更適用於所有 /std 模式,而且同時是來源和二進位中斷性變更。

下列範例示範編譯器行為在 16.1 和更新版本中變更的方式:

#include <type_traits>

enum E : unsigned char { e };

int f(unsigned int)
{
    return 1;
}

int f(unsigned char)
{
    return 2;
}

struct A {};
struct B : public A {};

int f(unsigned int, const B&)
{
    return 3;
}

int f(unsigned char, const A&)
{
    return 4;
}

int main()
{
    // Calls f(unsigned char) in 16.1 and later. Called f(unsigned int) in earlier versions.
    // The conversion from 'E' to the fixed underlying type 'unsigned char' is better than the
    // conversion from 'E' to the promoted type 'unsigned int'.
    f(e);
  
    // Error C2666. This call is ambiguous, but previously called f(unsigned int, const B&). 
    f(e, B{});
}

更新的新標準程式庫函式 (C++20)

  • 用於 basic_stringbasic_string_viewstarts_with()ends_with()
  • 關聯容器的 contains()
  • listforward_listremove()remove_if()unique() 現在會傳回 size_type
  • shift_left()shift_right() 已新增至 <algorithm>。

16.2 中的一致性改善

noexcept constexpr 函式

用於常數運算式中時,constexpr 函式預設已不再被視為 noexcept。 此行為變更來自核心工作群組 (CWG) CWG 1351 (英文) 的決議,並會以 /permissive- (部分機器翻譯) 啟用。 下列範例會在 Visual Studio 2019 16.1 版和更早版本中編譯,但在 Visual Studio 2019 16.2 版中會產生 C2338:

constexpr int f() { return 0; }

int main() {
    static_assert(noexcept(f()), "f should be noexcept"); // C2338 in 16.2
}

若要修正錯誤,請將 noexcept 運算式新增至函式宣告:

constexpr int f() noexcept { return 0; }

int main() {
    static_assert(noexcept(f()), "f should be noexcept");
}

具有不同列舉類型的二進位運算式

C++20 已取代運算元上的一般算術轉換,其中:

  • 其中一個運算元是列舉類型,而且

  • 另一個是不同的列舉類型或浮點類型。

如需詳細資訊,請參閱 P1120R0 (英文)。

在 Visual Studio 2019 16.2 版和更新版本中,下列程式碼會在啟用 /std:c++latest 編譯器選項時產生層級 4 C5054 警告 (在 Visual Studio 2019 16.11 版和更新版本中為 /std:c++20):

enum E1 { a };
enum E2 { b };
int main() {
    int i = a | b; // warning C5054: operator '|': deprecated between enumerations of different types
}

若要避免警告,請使用 static_cast (部分機器翻譯) 來轉換第二個運算元:

enum E1 { a };
enum E2 { b };
int main() {
  int i = a | static_cast<int>(b);
}

啟用 /std:c++latest 編譯器選項時 (在 Visual Studio 2019 16.11 版和更新版本中為 /std:c++20),在列舉與浮點類型之間使用二進位作業現在會是層級 1 C5055 警告:

enum E1 { a };
int main() {
  double i = a * 1.1;
}

若要避免警告,請使用 static_cast (部分機器翻譯) 來轉換第二個運算元:

enum E1 { a };
int main() {
   double i = static_cast<int>(a) * 1.1;
}

陣列的相等和關聯性比較

兩個陣列類型的運算元之間的相等和關聯性比較,在 C++20 中已淘汰 (P1120R0 (英文))。 換句話說,兩個陣列之間的比較作業 (儘管排名和範圍上的相似性) 現在會是警告。 在 Visual Studio 2019 16.2 版和更新版本中,下列程式碼會在啟用 /std:c++latest 編譯器選項時產生層級 1 警告 C5056 (在 Visual Studio 2019 16.11 版和更新版本中為 /std:c++20):

int main() {
    int a[] = { 1, 2, 3 };
    int b[] = { 1, 2, 3 };
    if (a == b) { return 1; } // warning C5056: operator '==': deprecated for array types
}

若要避免警告,您可以比較第一個元素的位址:

int main() {
    int a[] = { 1, 2, 3 };
    int b[] = { 1, 2, 3 };
    if (&a[0] == &b[0]) { return 1; }
}

若要判斷兩個陣列的內容是否相等,請使用 std::equal (部分機器翻譯) 函式:

std::equal(std::begin(a), std::end(a), std::begin(b), std::end(b));

定義太空船運算子對 ==!= 的影響

除非太空船運算子標示為 = default (P1185R2 (英文)),否則單獨定義太空船運算子 (<=>) 將不再重寫涉及 ==!= 的運算式。 下列範例會在 Visual Studio 2019 RTW 和 16.1 版中編譯,但在 Visual Studio 2019 16.2 版中會產生 C2678:

#include <compare>

struct S {
  int a;
  auto operator<=>(const S& rhs) const {
    return a <=> rhs.a;
  }
};
bool eq(const S& lhs, const S& rhs) {
  return lhs == rhs; // error C2676
}
bool neq(const S& lhs, const S& rhs) {
    return lhs != rhs; // error C2676
}

若要避免錯誤,請定義 operator== 或將其宣告為預設值:

#include <compare>

struct S {
  int a;
  auto operator<=>(const S& rhs) const {
    return a <=> rhs.a;
  }
  bool operator==(const S&) const = default;
};
bool eq(const S& lhs, const S& rhs) {
  return lhs == rhs;
}
bool neq(const S& lhs, const S& rhs) {
    return lhs != rhs;
}

標準程式庫改善

  • 具有固定/科學精確度的 <charconv>to_chars()。 (一般精確度目前規劃在 16.4 中推出)。
  • P0020R6 (英文):atomic<float>atomic<double>atomic<long double>
  • P0463R1 (英文):位元組順序
  • P0482R6 (英文):char8_t 的程式庫支援
  • P0600R1 (英文):[[nodiscard]] 適用於 STL,第 1 部分
  • P0653R2 (英文):to_address()
  • P0754R2 (英文):<version>
  • P0771R1 (英文):noexcept 適用於 std::function 的移動建構函式

關聯容器的 const 比較子

適用於 set (部分機器翻譯)、map (部分機器翻譯)、multiset (部分機器翻譯) 和 multimap (部分機器翻譯) 中搜尋和插入的程式碼已經合併在一起以縮小程式碼大小。 插入作業現在會呼叫 const 比較函式上的小於比較,其方式與先前的搜尋作業相同。 下列程式碼會在 Visual Studio 2019 16.1 版和更早版本中編譯,但在 Visual Studio 2019 16.2 版中會引發 C3848:

#include <iostream>
#include <map>

using namespace std;

struct K
{
   int a;
   string b = "label";
};

struct Comparer  {
   bool operator() (K a, K b) {
      return a.a < b.a;
   }
};

map<K, double, Comparer> m;

K const s1{1};
K const s2{2};
K const s3{3};

int main() {

   m.emplace(s1, 1.08);
   m.emplace(s2, 3.14);
   m.emplace(s3, 5.21);

}

若要避免錯誤,請將比較運算子 const

struct Comparer  {
   bool operator() (K a, K b) const {
      return a.a < b.a;
   }
};

Visual Studio 2019 16.3 版中的一致性改善

已移除 char* 的資料流擷取運算子

已移除指標到字元的資料流擷取運算子,並取代為字元陣列的擷取運算子 (根據 P0487R1 (英文))。 WG21 會將移除的多載視為不安全。 在 /std:c++20/std:c++latest 模式中,下列範例現在會產生 C2679:

// stream_extraction.cpp
// compile by using: cl /std:c++latest stream_extraction.cpp

#include <iostream>
#include <iomanip>

int main() {
    char x[42];
    char* p = x;
    std::cin >> std::setw(42);
    std::cin >> p;  // C2679: binary '>>': no operator found which takes a right-hand operand of type 'char *' (or there is no acceptable conversion)
}

若要避免錯誤,請使用擷取運算子搭配 char[] 變數:

#include <iostream>
#include <iomanip>

int main() {
    char x[42];
    std::cin >> std::setw(42);
    std::cin >> x;  // OK
}

新的關鍵字 requiresconcept

新的關鍵字 requiresconcept 已新增至 Microsoft C++ 編譯器。 如果您嘗試在 /std:c++20/std:c++latest 模式中使用其中一個作為識別碼,編譯器會引發 C2059 來表示語法錯誤。

不允許使用建構函式作為類型名稱

在下列情況下,編譯器不再將建構函式名稱視為 injected-class-names:當其在別名至類別範本特製化之後出現在限定名稱中時。 先前,建構函式是作為宣告其他實體的類型名稱。 下列範例現在會產生 C3646:

#include <chrono>

class Foo {
   std::chrono::milliseconds::duration TotalDuration{}; // C3646: 'TotalDuration': unknown override specifier
};

若要避免錯誤,請宣告 TotalDuration,如下所示:

#include <chrono>

class Foo {
  std::chrono::milliseconds TotalDuration {};
};

更嚴格的 extern "C" 函式檢查

如果 extern "C" 函式是在不同的命名空間中宣告,舊版 Microsoft C++ 編譯器並未檢查宣告是否相容。 在 Visual Studio 2019 16.3 版和更新版本中,編譯器會檢查相容性。 在 /permissive- (部分機器翻譯) 模式中,下列程式碼會產生錯誤 C2371 和 C2733:

using BOOL = int;

namespace N
{
   extern "C" void f(int, int, int, bool);
}

void g()
{
   N::f(0, 1, 2, false);
}

extern "C" void f(int, int, int, BOOL){}
    // C2116: 'N::f': function parameter lists do not match between declarations
    // C2733: 'f': you cannot overload a function with 'extern "C"' linkage

若要避免上一個範例中的錯誤,請在這兩個 f 的宣告中一致地使用 bool,而不是使用 BOOL

標準程式庫改善

已移除 <stdexcpt.h> 和 <typeinfo.h> 的非標準標頭。 包括這些標頭的程式碼應該分別包括標準標頭 <exception> 和 <typeinfo>。

Visual Studio 2019 16.4 版中的一致性改善

針對 /permissive- 中的限定識別碼提供更佳的雙階段名稱查閱強制

兩階段名稱查閱需要讓範本能夠在定義時看到範本主體中使用的非相依名稱。 先前,此類名稱可能會在將範本具現化時找到。 此變更可讓您更輕鬆地在 MSVC 中於 /permissive- (部分機器翻譯) 旗標底下撰寫可攜且一致的程式碼。

在已設定 /permissive- 旗標的 Visual Studio 2019 16.4 版中,下列範例會產生錯誤,因為在定義 f<T> 範本的情況下並無法看見 N::f

template <class T>
int f() {
    return N::f() + T{}; // error C2039: 'f': is not a member of 'N'
}

namespace N {
    int f() { return 42; }
}

通常,可以透過包括遺漏的標頭或轉送宣告函式或變數來修正此錯誤,如下列範例所示:

namespace N {
    int f();
}

template <class T>
int f() {
    return N::f() + T{};
}

namespace N {
    int f() { return 42; }
}

將整數常數運算式隱含轉換成 null 指標

MSVC 編譯器現在會在一致性模式中實作 CWG 問題 903 (/permissive- (英文))。 此規則不允許將整數常數運算式的隱含轉換 (整數常值 '0' 除外) 為 null 指標常數。 下列範例會在一致性模式中產生 C2440:

int* f(bool* p) {
    p = false; // error C2440: '=': cannot convert from 'bool' to 'bool *'
    p = 0; // OK
    return false; // error C2440: 'return': cannot convert from 'bool' to 'int *'
}

若要修正錯誤,請改為使用 nullptr,而非 false。 仍允許常值 0:

int* f(bool* p) {
    p = nullptr; // OK
    p = 0; // OK
    return nullptr; // OK
}

整數常值類型的標準規則

在一致性模式 (由 /permissive- (部分機器翻譯) 啟用) 中,MSVC 會使用整數常值類型的標準規則。 太大而無法納入 signed int 的十進位常值,先前會指定為 unsigned int 類型。 現在,此類常值會得到下一個最大的 signed 整數類型,long long。 此外,太大而無法納入 signed 類型且具有 'll' 尾碼的常值,則會指定為 unsigned long long 類型。

此變更可能會導致產生不同的警告診斷,以及常值算術運算上的行為差異。

下列範例顯示 Visual Studio 2019 16.4 版的新行為。 i 變數現在的類型為 unsigned int,因此會引發警告。 變數 j 的高順序位元會設定為 0。

void f(int r) {
    int i = 2964557531; // warning C4309: truncation of constant value
    long long j = 0x8000000000000000ll >> r; // literal is now unsigned, shift will fill high-order bits with 0
}

下列範例示範如何保留舊的行為,並避免警告和執行階段行為變更:

void f(int r) {
int i = 2964557531u; // OK
long long j = (long long)0x8000000000000000ll >> r; // shift will keep high-order bits
}

陰影範本參數的函式參數

MSVC 編譯器現在會在函式參數遮蔽範本參數時引發錯誤:

template<typename T>
void f(T* buffer, int size, int& size_read);

template<typename T, int Size>
void f(T(&buffer)[Size], int& Size) // error C7576: declaration of 'Size' shadows a template parameter
{
    return f(buffer, Size, Size);
}

若要修正錯誤,請變更其中一個參數的名稱:

template<typename T>
void f(T* buffer, int size, int& size_read);

template<typename T, int Size>
void f(T (&buffer)[Size], int& size_read)
{
    return f(buffer, Size, size_read);
}

使用者提供的類型特徵特製化

在與標準的 meta.rqmts 次子句一致的情況下,MSVC 編譯器現在會在其於 std 命名空間中找到其中一個指定 type_traits 範本的使用者定義特製化時引發錯誤。 除非另有指定,否則此類特製化會導致未定義的行為。 下列範例有未定義的行為,因為其違反規則,且 static_assert 會失敗並顯示錯誤 C2338。

#include <type_traits>
struct S;

template<>
struct std::is_fundamental<S> : std::true_type {};

static_assert(std::is_fundamental<S>::value, "fail");

若要避免錯誤,請定義繼承自慣用 type_trait 的結構,並對下列項目進行特製化:

#include <type_traits>

struct S;

template<typename T>
struct my_is_fundamental : std::is_fundamental<T> {};

template<>
struct my_is_fundamental<S> : std::true_type { };

static_assert(my_is_fundamental<S>::value, "fail");

對編譯器提供的比較運算子的變更

MSVC 編譯器現在會在啟用 /std:c++20/std:c++latest 選項時,根據 P1630R1 (英文) 對比較運算子實作下列變更:

在涉及非 bool 的傳回類型的情況下,編譯器不會再使用 operator== 重寫運算式。 下列程式碼現在會產生錯誤 C2088:

struct U {
    operator bool() const;
};

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

bool neq(const S& lhs, const S& rhs) {
    return lhs != rhs;  // C2088: '!=': illegal for struct
}

若要避免錯誤,您必須明確定義所需的運算子:

struct U {
    operator bool() const;
};

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

bool neq(const S& lhs, const S& rhs) {
    return lhs != rhs;
}

如果預設的比較運算子是類似等位類型的成員,編譯器將不會再加以定義。 下列範例現在會產生錯誤 C2120:

#include <compare>

union S {
    int a;
    char b;
    auto operator<=>(const S&) const = default;
};

bool lt(const S& lhs, const S& rhs) {
    return lhs < rhs;
}

若要避免錯誤,請為運算子定義主體:

#include <compare>

union S {
    int a;
    char b;
    auto operator<=>(const S&) const { ... }
};

bool lt(const S& lhs, const S& rhs) {
    return lhs < rhs;
}

如果類別包含參考成員,編譯器將不再定義預設的比較運算子。 下列程式碼現在會產生錯誤 C2120:

#include <compare>

struct U {
    int& a;
    auto operator<=>(const U&) const = default;
};

bool lt(const U& lhs, const U& rhs) {
    return lhs < rhs;
}

若要避免錯誤,請為運算子定義主體:

#include <compare>

struct U {
    int& a;
    auto operator<=>(const U&) const { ... };
};

bool lt(const U& lhs, const U& rhs) {
    return lhs < rhs;
}

Visual Studio 2019 16.5 版中的一致性改善

沒有初始設定式的明確特製化宣告不是定義

/permissive- 下,MSVC 現在會強制執行一個標準規則,其指出沒有初始設定式的明確特製化宣告不是定義。 先前,宣告會被視為具有預設初始設定式的定義。 效果可在連結時間觀察到,因為相依於此行為的程式現在可能會有無法解析的符號。 這個範例現在會產生錯誤:

template <typename> struct S {
    static int a;
};

// In permissive-, this declaration isn't a definition, and the program won't link.
template <> int S<char>::a;

int main() {
    return S<char>::a;
}
error LNK2019: unresolved external symbol "public: static int S<char>::a" (?a@?$S@D@@2HA) referenced in function _main at link time.

若要解決此問題,請新增初始設定式:

template <typename> struct S {
    static int a;
};

// Add an initializer for the declaration to be a definition.
template <> int S<char>::a{};

int main() {
    return S<char>::a;
}

前置處理器輸出會保留新行

實驗性前置處理器現在會在搭配 /experimental:preprocessor 使用 /P/E 時保留新行和空白。

以下列範例來源為例,

#define m()
line m(
) line

先前的 /E 輸出是:

line line
#line 2

/E 的新輸出現在是:

line
 line

importmodule 關鍵字會相依於內容

根據 P1857R1 (英文),importmodule 前置處理器指示詞的語法會有新的限制。 此範例不再編譯:

import // Invalid
m;     // error C2146: syntax error: missing ';' before identifier 'm'

若要解決此問題,請將匯入保留在同一行:

import m; // OK

移除 std::weak_equalitystd::strong_equality

P1959R0 (英文) 的合併需要編譯器移除針對 std::weak_equalitystd::strong_equality 類型的行為和參考。

此範例中的程式碼不會再編譯:

#include <compare>

struct S {
    std::strong_equality operator<=>(const S&) const = default;
};

void f() {
    nullptr<=>nullptr;
    &f <=> &f;
    &S::operator<=> <=> &S::operator<=>;
}

這個範例現在會導致下列錯誤:

error C2039: 'strong_equality': is not a member of 'std'
error C2143: syntax error: missing ';' before '<=>'
error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
error C7546: binary operator '<=>': unsupported operand types 'nullptr' and 'nullptr'
error C7546: binary operator '<=>': unsupported operand types 'void (__cdecl *)(void)' and 'void (__cdecl *)(void)'
error C7546: binary operator '<=>': unsupported operand types 'int (__thiscall S::* )(const S &) const' and 'int (__thiscall S::* )(const S &) const'

若要解決此問題,請更新為偏好使用內建關係運算子,並取代已移除的類型:

#include <compare>

struct S {
    std::strong_ordering operator<=>(const S&) const = default; // prefer 'std::strong_ordering'
};

void f() {
    nullptr != nullptr; // use pre-existing builtin operator != or ==.
    &f != &f;
    &S::operator<=> != &S::operator<=>;
}

TLS 防護變更

先前,DLL 中的執行緒區域變數未正確初始化。 除了在載入 DLL 的執行緒上,其在載入 DLL 之前存在的執行緒上首次使用之前將不會初始化。 此缺陷現已修正。 此類 DLL 中的執行緒區域變數會在其餘此類執行緒上首次使用之前立即初始化。

這個使用執行緒區域變數來測試初始化的新行為,可以使用 /Zc:tlsGuards- 編譯器選項來停用。 或者,透過將 [[msvc:no_tls_guard]] 屬性新增至特定執行緒區域變數。

對刪除函式之呼叫的更佳診斷

我們的編譯器先前對已刪除函式的呼叫是更寬鬆的。 例如,如果呼叫發生在範本主體的內容中,我們便不會診斷呼叫。 此外,如果有多個對已刪除函式的呼叫,我們只會發出一個診斷。 現在我們會為每個呼叫發出診斷。

新行為的其中一個後果可能會產生一個小型的中斷性變更:如果呼叫已刪除函式的程式碼從未需要用於程式碼產生,則將不會加以診斷。 現在我們會在一開始就加以診斷。

這個範例顯示該程式碼現在會產生錯誤:

struct S {
  S() = delete;
  S(int) { }
};

struct U {
  U() = delete;
  U(int i): s{ i } { }

  S s{};
};

U u{ 0 };
error C2280: 'S::S(void)': attempting to reference a deleted function
note: see declaration of 'S::S'
note: 'S::S(void)': function was explicitly deleted

若要解決此問題,請移除對已刪除函式的呼叫:

struct S {
  S() = delete;
  S(int) { }
};

struct U {
  U() = delete;
  U(int i): s{ i } { }

  S s;  // Do not call the deleted ctor of 'S'.
};

U u{ 0 };

Visual Studio 2019 16.6 版中的一致性改善

標準程式庫資料流會拒絕插入編碼錯誤的字元類型

傳統上,將 wchar_t 插入 std::ostream,並將 char16_tchar32_t 插入 std::ostreamstd::wostream,會輸出其整數值。 插入那些字元類型的指標會輸出指標值。 這兩個案例對程式設計師來說都不夠直覺。 他們通常會預期標準程式庫會改為對字元或以 null 結尾的字元字串進行轉碼,並輸出結果。

C++20 提案 P1423R3 (英文) 為資料流和字元的組合或字元指標類型新增已刪除的資料流插入運算子多載。 在 /std:c++20/std:c++latest 下,多載會使這些插入格式不正確,而不是以非預期的方式行事。 當找到錯誤時,編譯器就會引發錯誤 C2280。 您可以將「逃生口」巨集 _HAS_STREAM_INSERTION_OPERATORS_DELETED_IN_CXX20 定義為 1 以還原舊的行為。 (該提案也會刪除 char8_t 的資料流插入運算子。當我們新增 char8_t 支援時,我們的標準程式庫會實作類似的多載,因此從未針對 char8_t 提供「錯誤」的行為。)

此範例顯示此變更的行為:

#include <iostream>
int main() {
    const wchar_t cw = L'x', *pw = L"meow";
    const char16_t c16 = u'x', *p16 = u"meow";
    const char32_t c32 = U'x', *p32 = U"meow";
    std::cout << cw << ' ' << pw << '\n';
    std::cout << c16 << ' ' << p16 << '\n';
    std::cout << c32 << ' ' << p32 << '\n';
    std::wcout << c16 << ' ' << p16 << '\n';
    std::wcout << c32 << ' ' << p32 << '\n';
}

程式碼現在會產生下列診斷訊息:

error C2280: 'std::basic_ostream<char,std::char_traits<char>> &std::<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,wchar_t)': attempting to reference a deleted function
error C2280: 'std::basic_ostream<char,std::char_traits<char>> &std::<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char16_t)': attempting to reference a deleted function
error C2280: 'std::basic_ostream<char,std::char_traits<char>> &std::<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char32_t)': attempting to reference a deleted function
error C2280: 'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::<<<std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,char16_t)': attempting to reference a deleted function
error C2280: 'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::<<<std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,char32_t)': attempting to reference a deleted function

您可以將字元類型轉換成 unsigned int,或將指標到字元類型轉換為 const void*,以在所有語言模式中達成舊行為的效果:

#include <iostream>
int main() {
    const wchar_t cw = L'x', *pw = L"meow";
    const char16_t c16 = u'x', *p16 = u"meow";
    const char32_t c32 = U'x', *p32 = U"meow";
    std::cout << (unsigned)cw << ' ' << (const void*)pw << '\n'; // Outputs "120 0052B1C0"
    std::cout << (unsigned)c16 << ' ' << (const void*)p16 << '\n'; // Outputs "120 0052B1CC"
    std::cout << (unsigned)c32 << ' ' << (const void*)p32 << '\n'; // Outputs "120 0052B1D8"
    std::wcout << (unsigned)c16 << ' ' << (const void*)p16 << '\n'; // Outputs "120 0052B1CC"
    std::wcout << (unsigned)c32 << ' ' << (const void*)p32 << '\n'; // Outputs "120 0052B1D8"
}

已變更 std::complexstd::pow() 的傳回類型

先前,函式範本傳回類型之升級規則的 MSVC 實作 std::pow() 是不正確的。 例如,先前 pow(complex<float>, int) 會傳回 complex<float>。 現在其會正確地傳回 complex<double>。 已針對 Visual Studio 2019 16.6 版中的所有標準模式無條件實作修正。

此變更可能會導致編譯器錯誤。 例如,您先前可以將 pow(complex<float>, int) 乘以 float。 由於 complex<T> operator* 預期相同類型的引數,下列範例現在會發出編譯器錯誤 C2676:

// pow_error.cpp
// compile by using: cl /EHsc /nologo /W4 pow_error.cpp
#include <complex>

int main() {
    std::complex<float> cf(2.0f, 0.0f);
    (void) (std::pow(cf, -1) * 3.0f);
}
pow_error.cpp(7): error C2676: binary '*': 'std::complex<double>' does not define this operator or a conversion to a type acceptable to the predefined operator

有許多可能的修正:

  • float 被乘數的類型變更為 double。 這個引數可以直接轉換成 complex<double>,以符合 pow 所傳回的類型。

  • 透過表示 complex<float>{pow(ARG, ARG)},將 pow 的結果縮小到 complex<float>。 然後,您可以繼續乘以 float 值。

  • float,而不是將 int 傳遞至 pow。 此作業可能會變慢。

  • 在某些情況下,您可以完全避開 pow。 例如,pow(cf, -1) 可以取代為除法。

C 的 switch 警告

在 Visual Studio 2019 16.6 版和更新版本中,編譯器會針對編譯為 C 的程式碼實作一些預先存在的 C++ 警告。下列警告現在會在不同層級啟用:C4060、C4061、C4062、C4063、C4064、C4065、C4808 和 C4809。 C 預設會停用 C4065 和 C4060 的警告。

警告會在遺漏 case 陳述式、未定義 enum,以及有錯誤的 bool 切換陳述式 (也就是包含太多案例的陳述式) 時觸發。 例如:

#include <stdbool.h>

int main() {
    bool b = true;
    switch (b) {
        case true: break;
        case false: break;
        default: break; // C4809: switch statement has redundant 'default' label;
                        // all possible 'case' labels are given
    }
}

若要修正此程式碼,請移除備援 default 案例:

#include <stdbool.h>

int main() {
    bool b = true;
    switch (b) {
        case true: break;
        case false: break;
    }
}

typedef 宣告中的未命名類別

在 Visual Studio 2019 16.6 版和更新版本中,typedef 宣告的行為已加以限制,以符合 P1766R1 (英文)。 透過此更新,typedef 宣告內的未命名類別除了下列成員之外,不能有任何成員:

  • 沒有預設成員初始設定式的非靜態資料成員、
  • 成員類別,或
  • 成員列舉。

相同的限制會以遞歸方式套用至每個巢狀類別。 此限制旨在確保具有 typedef 名稱以用於連結目的之結構的簡易性。 在編譯器取得連結的 typedef 名稱之前,其必須簡單到不需要進行任何連結計算。

此變更會影響編譯器的所有標準模式。 在預設 (/std:c++14) 和 /std:c++17 模式中,編譯器會針對不一致的程式碼發出警告 C5208。 如果指定 /permissive-,編譯器會在 /std:c++14 下發出警告 C5208 作為錯誤,並在 /std:c++17 下發出錯誤 C7626。 當指定 /std:c++20/std:c++latest 時,編譯器會針對不一致的程式碼發出錯誤 C7626。

下列範例顯示未命名結構中不再允許的建構。 根據指定的標準模式,會發出 C5208 或 C7626 錯誤或警告:

struct B { };
typedef struct : B { // inheriting from 'B'; ill-formed
    void f(); // ill-formed
    static int i; // ill-formed
    struct U {
        void f(); // nested class has non-data member; ill-formed
    };
    int j = 10; // default member initializer; ill-formed
} S;

您可以透過為未命名的類別提供名稱來修正上述程式碼:

struct B { };
typedef struct S_ : B {
    void f();
    static int i;
    struct U {
        void f();
    };
    int j = 10;
} S;

C++/CLI 中的預設引數匯入

有越來越多的 API 在 .NET Core 中具有預設引數。 因此,我們現在支援 C++/CLI 中的預設引數匯入。 此變更可能會中斷宣告多個多載的現有程式碼,如下列範例所示:

public class R {
    public void Func(string s) {}   // overload 1
    public void Func(string s, string s2 = "") {} // overload 2;
}

當這個類別匯入 C++/CLI 時,對其中一個多載進行呼叫會造成錯誤:

    (gcnew R)->Func("abc"); // error C2668: 'R::Func' ambiguous call to overloaded function

編譯器會發出錯誤 C2668,因為兩個多載都符合這個引數清單。 在第二個多載中,預設引數會填入第二個引數。 若要解決此問題,您可以刪除備援多載 (1)。 或者,使用完整的引數清單,並明確提供預設引數。

Visual Studio 2019 16.7 版中的一致性改善

is trivially copyable 定義

C++20 已變更 s trivially copyable 的定義。 當類別具備有 volatile 限定類型的非靜態資料成員時,其不再表示任何編譯器產生的複製或移動建構函式,或複製或移動指派運算子都是非一般的。 C++ 標準委員會將此變更追溯性地套用為瑕疵報告。 在 MSVC 中,編譯器行為不會在不同的語言模式中變更,例如 /std:c++14/std:c++latest

以下是新行為的範例:

#include <type_traits>

struct S
{
    volatile int m;
};

static_assert(std::is_trivially_copyable_v<S>, "Meow!");

在 Visual Studio 2019 16.7 版之前,此程式碼不會在 MSVC 的版本中編譯。 有預設關閉 (部分機器翻譯) 的編譯器警告,可用來偵測此變更。 如果您使用 cl /W4 /w45220 來編譯上述程式碼,您會看到下列警告:

warning C5220: `'S::m': a non-static data member with a volatile qualified type no longer implies that compiler generated copy/move constructors and copy/move assignment operators are non trivial`

針對 bool 的成員指標和字串常值轉換正在縮小

C++ 標準委員會最近採用了缺陷報告 P1957R2 (英文),該報告將 T*bool 視為縮小轉換。 MSVC 修正了其實作中的 Bug,其先前會將 T*bool 診斷為縮小,但並不會診斷字串常值至 bool 或成員指標至 bool 的轉換。

下列程式在 Visual Studio 2019 16.7 版中格式不正確:

struct X { bool b; };
void f(X);

int main() {
    f(X { "whoops?" }); // error: conversion from 'const char [8]' to 'bool' requires a narrowing conversion

    int (X::* p) = nullptr;
    f(X { p }); // error: conversion from 'int X::*' to 'bool' requires a narrowing conversion
}

若要更正此程式碼,請將明確比較新增至 nullptr,或避免縮小轉換格式不正確的內容:

struct X { bool b; };
void f(X);

int main() {
    f(X { "whoops?" != nullptr }); // Absurd, but OK

    int (X::* p) = nullptr;
    f(X { p != nullptr }); // OK
}

nullptr_t 只能以直接初始化的形式轉換為 bool

在 C++11 中,nullptr 只能以直接轉換的形式轉換成 bool;例如,當您使用括號的初始設定式清單來將 bool 初始化時。 MSVC 從未強制執行此限制。 MSVC 現在會在 /permissive- (部分機器翻譯) 下實作該規則。 隱含轉換現在會診斷為格式不正確。 仍允許針對 bool 的內容轉換,因為直接初始化 bool b(nullptr) 是有效的。

在大部分情況下,您可以將 nullptr 取代為 false 來修正錯誤,如下列範例所示:

struct S { bool b; };
void g(bool);
bool h() { return nullptr; } // error, should be 'return false;'

int main() {
    bool b1 = nullptr; // error: cannot convert from 'nullptr' to 'bool'
    S s { nullptr }; // error: cannot convert from 'nullptr' to 'bool'
    g(nullptr); // error: cannot convert argument 1 from 'nullptr' to 'bool'

    bool b2 { nullptr }; // OK: Direct-initialization
    if (!nullptr) {} // OK: Contextual conversion to bool
}

遺漏初始設定式之陣列初始化的一致初始化行為

先前,MSVC 對於遺漏初始設定式的陣列初始化的行為不一致。 MSVC 一律會針對沒有初始設定式的每個陣列元素呼叫預設建構函式。 標準行為是使用空白以大括號括住的初始設定式清單 ({}) 將每個元素初始化。 空白以大括號括住的初始設定式清單的初始化內容是複製初始化,其不允許呼叫明確建構函式。 也可能會有執行階段差異,因為使用 {} 進行初始化可能會呼叫採用 std::initializer_list 的建構函式,而不是預設建構函式。 符合的行為會在 /permissive- (部分機器翻譯) 下啟用。

以下是已變更行為的範例:

struct B {
    explicit B() {}
};

void f() {
    B b1[1]{}; // Error in /permissive-, because aggregate init calls explicit ctor
    B b2[1]; // OK: calls default ctor for each array element
}

已對具有多載名稱之類別成員的初始化進行正確排序

我們識別出當類型名稱也多載為資料成員的名稱時,類別資料成員內部表示法中的 Bug。 這個 Bug 會導致彙總初始化和成員初始化順序的不一致。 產生的初始化程式碼現在已正確無誤。 不過,此變更可能會導致來源中不小心依賴錯誤排序成員的錯誤或警告,如下列範例所示:

// Compiling with /w15038 now gives:
// warning C5038: data member 'Outer::Inner' will be initialized after data member 'Outer::v'
struct Outer {
    Outer(int i, int j) : Inner{ i }, v{ j } {}

    struct Inner { int x; };
    int v;
    Inner Inner; // 'Inner' is both a type name and data member name in the same scope
};

在舊版中,建構函式會在資料成員 v 之前,錯誤地將資料成員 Inner 初始化。 (C++ 標準要求使用與成員宣告順序相同的初始化順序)。 既然產生的程式碼遵循標準,便代表成員初始化清單不符合順序。 編譯器會針對這個範例產生警告。 若要修正此問題,請將成員初始設定式清單重新排序以反映宣告順序。

涉及整數多載和 long 引數的多載解析

C++ 標準要求將 longint 的轉換以標準轉換的形式排名。 先前的 MSVC 編譯器會錯誤地將其排名為整數提升,這會使其排名比多載解析較高。 此排名可能會導致多載解析在應視為模棱兩可的情況下成功解析。

編譯器現在會以 /permissive- (部分機器翻譯) 模式正確考慮排名。 無效的程式碼會正確診斷,如下列範例所示:

void f(long long);
void f(int);

int main() {
    long x {};
    f(x); // error: 'f': ambiguous call to overloaded function
    f(static_cast<int>(x)); // OK
}

您可以透過數種方式來修正此問題:

  • 在呼叫位置,將傳遞的引數類型變更為 int。 您可以變更變數類型,或加以轉換。

  • 如果有許多呼叫位置,您可以新增另一個採用 long 引數的多載。 在此函式中,轉換引數並將其轉寄至 int 多載。

搭配內部連結使用未定義的變數

Visual Studio 2019 16.7 版之前的 MSVC 版本接受使用宣告變數的 extern,其具有內部連結且尚未定義。 此類變數無法在任何其他轉譯單位中定義,而且無法形成有效的程式。 編譯器現在會在編譯時間診斷此案例。 此錯誤類似於未定義靜態函式的錯誤。

namespace {
    extern int x; // Not a definition, but has internal linkage because of the anonymous namespace
}

int main()
{
    return x; // Use of 'x' that no other translation unit can possibly define.
}

此程式先前會不正確地編譯及連結,但現在會發出錯誤 C7631。

error C7631: 'anonymous-namespace::x': variable with internal linkage declared but not defined

此類變數必須在其使用所在的相同轉譯單位中定義。 例如,您可以提供明確的初始設定式或個別定義。

類型完整性和衍生至基底指標轉換

在 C++20 之前的 C++ 標準中,從衍生類別轉換成基底類別並不需要衍生類別是完整的類別類型。 C++ 標準委員會已核准適用於所有 C++ 語言版本的追溯性缺陷報告變更。 此變更會使轉換流程與類型特徵一致,例如 std::is_base_of,其確實需要衍生類別是完整的類別類型。

以下是範例:

template<typename A, typename B>
struct check_derived_from
{
    static A a;
    static constexpr B* p = &a;
};

struct W { };
struct X { };
struct Y { };

// With this change this code will fail as Z1 is not a complete class type
struct Z1 : X, check_derived_from<Z1, X>
{
};

// This code failed before and it will still fail after this change
struct Z2 : check_derived_from<Z2, Y>, Y
{
};

// With this change this code will fail as Z3 is not a complete class type
struct Z3 : W
{
    check_derived_from<Z3, W> cdf;
};

此行為變更適用於 MSVC 的所有 C++ 語言模式,而不只是 /std:c++20/std:c++latest

縮小轉換會更加一致地診斷

MSVC 會在以大括號括住的清單初始設定式中針對縮小轉換發出警告。 先前,編譯器不會診斷從較大 enum 底層類型至較小整數類型的縮小轉換。 (編譯器會錯誤地將其視為整數提升,而不是轉換)。 如果您是刻意進行縮小轉換,可以在初始設定式引數上使用 static_cast 來避免警告。 或者,選擇較大的目的地整數類型。

以下是使用明確 static_cast 來解決警告的範例:

enum E : long long { e1 };
struct S { int i; };

void f(E e) {
    S s = { e }; // warning: conversion from 'E' to 'int' requires a narrowing conversion
    S s1 = { static_cast<int>(e) }; // Suppress warning with explicit conversion
}

Visual Studio 2019 16.8 版中的一致性改善

「類別右值作為左值使用」延伸項目

MSVC 有一個延伸模組,允許使用類別右值作為左值。 延伸模組不會延長類別右值的存留期,而且可能會導致執行階段未定義的行為。 我們現在會強制執行標準規則,並在 /permissive- 下不允許此延伸模組。 如果您還無法使用 /permissive-,可以使用 /we4238 來明確不允許該延伸模組。 以下是範例:

// Compiling with /permissive- now gives:
// error C2102: '&' requires l-value
struct S {};

S f();

void g()
{
    auto p1 = &(f()); // The temporary returned by 'f' is destructed after this statement. So 'p1' points to an invalid object.

    const auto &r = f(); // This extends the lifetime of the temporary returned by 'f'
    auto p2 = &r; // 'p2' points to a valid object
}

「非命名空間範圍中的明確特製化」延伸模組

MSVC 有一個延伸模組,允許在非命名空間範圍中進行明確特製化。 在 CWG 727 決議之後,其現在已是標準的一部分。 不過,存在行為上的差異。 我們已調整編譯器的行為,以符合標準。

// Compiling with 'cl a.cpp b.cpp /permissive-' now gives:
//   error LNK2005: "public: void __thiscall S::f<int>(int)" (??$f@H@S@@QAEXH@Z) already defined in a.obj
// To fix the linker error,
// 1. Mark the explicit specialization with 'inline' explicitly. Or,
// 2. Move its definition to a source file.

// common.h
struct S {
    template<typename T> void f(T);
    template<> void f(int);
};

// This explicit specialization is implicitly inline in the default mode.
template<> void S::f(int) {}

// a.cpp
#include "common.h"

int main() {}

// b.cpp
#include "common.h"

檢查抽象類別類型

C++20 標準已變更編譯器用來偵測使用抽象類別類型作為函式參數的流程。 具體而言,其不再是 SFINAE 錯誤。 先前,如果編譯器偵測到函式範本的特製化會有抽象類別類型執行個體作為函式參數,則該特製化會被視為格式不正確。 其不會新增至一組可行的候選函式。 在 C++20 中,在呼叫函式之前,不會檢查抽象類別類型的參數。 作為結果,用來編譯的程式碼不會造成錯誤。 以下是範例:

class Node {
public:
    int index() const;
};

class String : public Node {
public:
    virtual int size() const = 0;
};

class Identifier : public Node {
public:
    const String& string() const;
};

template<typename T>
int compare(T x, T y)
{
    return x < y ? -1 : (x > y ? 1 : 0);
}

int compare(const Node& x, const Node& y)
{
    return compare(x.index(), y.index());
}

int f(const Identifier& x, const String& y)
{
    return compare(x.string(), y);
}

先前,對 compare 的呼叫會嘗試針對 T 使用 String 範本引數來將函式範本 compare 特製化。 因為 String 是抽象類別,所以無法產生有效的特製化。 唯一可行的候選是 compare(const Node&, const Node&)。 不過,在 C++20 底下,在呼叫函式之前,不會檢查抽象類別類型。 因此,特製化 compare(String, String) 會新增至一組可行的候選,而且會被選擇為最佳候選,因為從 const String& 轉換成 String 的轉換順序比從 const String& 轉換成 const Node& 更好。

在 C++20 下,此範例的其中一個可能修正是使用概念;也就是說,將 compare 的定義變更為:

template<typename T>
int compare(T x, T y) requires !std::is_abstract_v<T>
{
    return x < y ? -1 : (x > y ? 1 : 0);
}

或者,如果 C++ 概念無法使用,您可以回復為使用 SFINAE:

template<typename T, std::enable_if_t<!std::is_abstract_v<T>, int> = 0>
int compare(T x, T y)
{
    return x < y ? -1 : (x > y ? 1 : 0);
}

針對 P0960R3 的支援 - 允許從以括弧括住的值清單進行彙總初始化

C++20 P0960R3 (英文) 新增使用以括弧括住的初始設定式清單進行彙總初始化的支援。 例如,下列程式碼在 C++20 中是有效的:

struct S {
    int i;
    int j;
};

S s(1, 2);

這項功能大部分都是加總的,也就是說,程式碼現在會編譯之前未編譯的內容。 不過,其確實會變更 std::is_constructible 的行為。 在 C++17 模式中,這個 static_assert 會失敗,但在 C++20 模式中,它會成功:

static_assert(std::is_constructible_v<S, int, int>, "Assertion failed!");

如果您使用此類型特徵來控制多載解析,其可能會導致 C++17 和 C++20 之間的行為變更。

涉及函式範本的多載解析

先前,編譯器允許某些程式碼在不應該編譯的 /permissive- 下編譯。 作為結果,編譯器會呼叫錯誤函式,導致執行階段行為發生變更:

int f(int);

namespace N
{
    using ::f;
    template<typename T>
    T f(T);
}

template<typename T>
void g(T&& t)
{
}

void h()
{
    using namespace N;
    g(f);
}

g 的呼叫會使用多載集合,其中包含兩個函式,::fN::f。 由於 N::f 是函式範本,因此編譯器應將函式引數視為非推斷內容。 這表示在此情況下,對 g 的呼叫應該會失敗,因為編譯器無法推斷範本參數 T 的類型。 可惜的是,編譯器並未捨棄其已經決定 ::f 與函式呼叫相符的事實。 編譯器不會發出錯誤,而是會產生程式碼以使用 ::f 作為引數來呼叫 g

由於在許多情況下,使用 ::f 作為函式引數都是使用者預期的結果,因此我們只會在程式碼是以 /permissive- 編譯時才會發出錯誤。

/await 移轉至 C++20 協同程式

標準 C++20 協同程式現在預設會在 /std:c++20/std:c++latest 下開啟。 其與協同程式 TS 和 /await 選項下的支援不同。 從 /await 移轉至標準協同程式可能需要進行一些來源變更。

非標準關鍵字

C++20 模式不支援舊的 awaityield 關鍵字。 程式碼必須改為使用 co_awaitco_yield。 標準模式也不允許在協同程式中使用 return。 協同程式中的每個 return 都必須使用 co_return

// /await
task f_legacy() {
    ...
    await g();
    return n;
}
// /std:c++latest
task f() {
    ...
    co_await g();
    co_return n;
}

initial_suspend/final_suspend 的類型

/await 下,promise 初始函式和暫止函式可能會宣告為傳回 bool。 此行為並非標準。 在 C++20 中,這些函式必須傳回可等候的類別類型,其通常是其中一個一般的可等候類型:std::suspend_always (如果函式先前傳回 true),或是 std::suspend_never (如果是傳回 false)。

// /await
struct promise_type_legacy {
    bool initial_suspend() noexcept { return false; }
    bool final_suspend() noexcept { return true; }
    ...
};

// /std:c++latest
struct promise_type {
    auto initial_suspend() noexcept { return std::suspend_never{}; }
    auto final_suspend() noexcept { return std::suspend_always{}; }
    ...
};

yield_value 的類型

在 C++20 中,promise yield_value 函式必須傳回可等候的類型。 在 /await 模式中,允許 yield_value 函式傳回 void,而且一律會暫止。 此類函式可以取代為傳回 std::suspend_always 的函式。

// /await
struct promise_type_legacy {
    ...
    void yield_value(int x) { next = x; };
};

// /std:c++latest
struct promise_type {
    ...
    auto yield_value(int x) { next = x; return std::suspend_always{}; }
};

例外狀況處理函式

/await 支援沒有例外狀況處理函式或名為 set_exception 且接受 std::exception_ptr 的例外狀況處理函式的 promise 類型。 在 C++20 中,promise 類型必須具有名為 unhandled_exception 的函式,而該函式不接受任何引數。 如有需要,可以從 std::current_exception 取得例外狀況物件。

// /await
struct promise_type_legacy {
    void set_exception(std::exception_ptr e) { saved_exception = e; }
    ...
};
// /std:c++latest
struct promise_type {
    void unhandled_exception() { saved_exception = std::current_exception(); }
    ...
};

不支援推斷的協同程式傳回類型

C++20 不支援包括預留位置類型之傳回類型的協同程式,例如 auto。 必須明確宣告協同程式的傳回類型。 在 /await 底下,這些推斷類型一律牽涉到實驗類型,且需要包括定義必要類型的標頭:std::experimental::task<T>std::experimental::generator<T>std::experimental::async_stream<T> 之一。

// /await
auto my_generator() {
    ...
    co_yield next;
};

// /std:c++latest
#include <experimental/generator>
std::experimental::generator<int> my_generator() {
    ...
    co_yield next;
};

傳回 return_value 的型別

promise return_value 函式的傳回類型必須是 void。 在 /await 模式中,傳回類型可以是任何項目,而且會被忽略。 此診斷可協助偵測細微的錯誤,例如當作者錯誤假設 return_value 的傳回值是傳回給呼叫者時。

// /await
struct promise_type_legacy {
    ...
    int return_value(int x) { return x; } // incorrect, the return value of this function is unused and the value is lost.
};

// /std:c++latest
struct promise_type {
    ...
    void return_value(int x) { value = x; }; // save return value
};

傳回物件轉換行為

如果協同程式的宣告傳回類型不符合 promise get_return_object 函式的傳回類型,則從 get_return_object 傳回的物件會轉換成協同程式的傳回類型。 在 /await 下,此轉換會提前完成,然後協同程式主體才有機會執行。 在 /std:c++20/std:c++latest 中,當值傳回給呼叫者時,就會完成此轉換。 其允許不會在初始暫止點暫停的協同程式,以使用協同程式主體內的 get_return_object 所傳回的物件。

協同程式 promise 參數

在 C++20 中,編譯器會嘗試將協同程式參數 (如果有的話) 傳遞至 promise 類型的建構函式。 如果失敗,其會使用預設建構函式重試。 在 /await 模式中,只會使用預設建構函式。 如果 promise 有多個建構函式,此變更可能會導致行為差異。 或者,如果有從協同程式參數轉換成 promise 類型的轉換。

struct coro {
    struct promise_type {
        promise_type() { ... }
        promise_type(int x) { ... }
        ...
    };
};

coro f1(int x);

// Under /await the promise gets constructed using the default constructor.
// Under /std:c++latest the promise gets constructed using the 1-argument constructor.
f1(0);

struct Object {
template <typename T> operator T() { ... } // Converts to anything!
};

coro f2(Object o);

// Under /await the promise gets constructed using the default constructor
// Under /std:c++latest the promise gets copy- or move-constructed from the result of
// Object::operator coro::promise_type().
f2(Object{});

/permissive- 和 C++20 模組預設會在 /std:c++20 下開啟

根據預設,C++20 模組支援預設會在 /std:c++20/std:c++latest 下開啟。 如需此變更的詳細資訊,以及會有條件地將 moduleimport 視為關鍵字的案例,請參閱 Visual Studio 2019 16.8 版中搭配 MSVC 的標準 C++20 模組支援 (英文)。

作為模組支援的必要條件,permissive- 現在會在指定 /std:c++20/std:c++latest 時啟用。 如需詳細資訊,請參閱/permissive-

針對先前在 /std:c++latest 下編譯且需要不一致編譯器行為的程式碼,可以指定 /permissive 來關閉編譯器中的嚴格一致性模式。 編譯器選項必須出現在命令列引數清單中的 /std:c++latest 之後。 不過,如果偵測到模組使用量,/permissive 會導致錯誤:

錯誤 C1214:模組與透過 'option' 所要求的非標準行為相衝突

option 最常見的值為:

選項 描述
/Zc:twoPhase- C++20 模組需要兩階段名稱查閱,且由 /permissive- 所表示。
/Zc:hiddenFriend- C++20 模組需要標準隱藏的 friend 名稱查閱規則,且由 /permissive- 所表示。
/Zc:lambda- C++20 模組需要標準 Lambda 處理,且由 /std:c++20 模式或更新版本所表示。
/Zc:preprocessor- 只有 C++20 標頭單位使用量和建立,才需要一致的前置處理器。 具名模組不需要此選項。

仍然需要 /experimental:module (部分機器翻譯) 選項,才能使用隨附於 Visual Studio 的 std.* 模組,因為其尚未標準化。

/experimental:module 選項也表示 /Zc:twoPhase/Zc:lambda/Zc:hiddenFriend。 先前,在僅使用模組的情況下,使用模組編譯的程式碼有時會使用 /Zc:twoPhase- 來編譯。 已不再支援此行為。

Visual Studio 2019 16.9 版中的一致性改善

參考直接初始化中暫存的複製初始化

核心工作群組問題 CWG 2267 (英文) 會處理以括號括住的初始設定式清單和以大括號括住的初始設定式清單之間的不一致。 該決議統一了兩種形式。

Visual Studio 2019 16.9 版會在所有 /std 編譯器模式中實作已變更的行為。 不過,因為其可能是來源中斷性變更,所以只有在使用 /permissive- 編譯程式碼時才加以支援。

此範例示範行為變更:

struct A { };

struct B {
    explicit B(const A&);
};

void f()
{
    A a;
    const B& b1(a);     // Always an error
    const B& b2{ a };   // Allowed before resolution to CWG 2267 was adopted: now an error
}

解構函式特性和可能建構的子物件

核心工作群組問題 CWG 2336 (英文) 涵蓋具有虛擬基底類別之類別中的解構函式隱含例外狀況規格的遺漏。 遺漏表示衍生類別中的解構函式可能會有比基底類別更弱的例外狀況規格,如果該基底已抽象化且具有 virtual 基底。

Visual Studio 2019 16.9 版會在所有 /std 編譯器模式中實作已變更的行為。

此範例示範解譯如何變更:

class V {
public:
    virtual ~V() noexcept(false);
};

class B : virtual V {
    virtual void foo () = 0;
    // BEFORE: implicitly defined virtual ~B() noexcept(true);
    // AFTER: implicitly defined virtual ~B() noexcept(false);
};

class D : B {
    virtual void foo ();
    // implicitly defined virtual ~D () noexcept(false);
};

在此變更之前,B 的隱含定義解構函式為 noexcept,因為只會考慮可能建構的子物件。 而且,基底類別 V 不是可能建構的子物件,因為其為 virtual 基底,且 B 是抽象的。 不過,基底類別 V 是類別 D 的可能建構子物件,因此 D::~D 會判斷為 noexcept(false),導致衍生類別的例外狀況規格比其基底要弱。 此解譯是不安全的。 如果例外狀況從衍生自 B 之類別的解構函式擲回,則可能會導致不正確的執行階段行為。

透過此變更,如果解構函式具有虛擬解構函式,而且任何虛擬基底類別都有可能擲回解構函式,則解構函式也可能會擲回。

類似的類型和參考繫結

核心工作群組問題 CWG 2352 (英文) 會處理參考繫結規則與類型相似性變更之間的不一致。 先前的瑕疵報告 (例如 CWG 330 (英文)) 導入了這個不一致的情況。 這會影響 Visual Studio 2019 16.0 到 16.8 版。

透過此變更,從 Visual Studio 2019 16.9 版開始,先前將參考繫結至 Visual Studio 2019 16.0 版至 16.8 版中暫存的程式碼,現在只要涉及的類型與 cv 限定詞不同時,就可以直接繫結。

Visual Studio 2019 16.9 版會在所有 /std 編譯器模式中實作已變更的行為。 其可能是來源中斷性變更。

如需相關變更,請參閱具有不相符 cv 限定詞之類型的參考

此範例顯示已變更的行為:

int *ptr;
const int *const &f() {
    return ptr; // Now returns a reference to 'ptr' directly.
    // Previously returned a reference to a temporary and emitted C4172
}

更新可能會變更仰賴所引進暫存的程式行為:

int func() {
    int i1 = 13;
    int i2 = 23;
    
    int* iptr = &i1;
    int const * const&  iptrcref = iptr;

    // iptrcref is a reference to a pointer to i1 with value 13.
    if (*iptrcref != 13)
    {
        return 1;
    }
    
    // Now change what iptr points to.

    // Prior to CWG 2352 iptrcref should be bound to a temporary and still points to the value 13.
    // After CWG 2352 it is bound directly to iptr and now points to the value 23.
    iptr = &i2;
    if (*iptrcref != 23)
    {
        return 1;
    }

    return 0;
}

/Zc:twoPhase/Zc:twoPhase- 選項行為變更

一般而言,MSVC 編譯器選項會以「最後一個看到獲勝」的原則運作。 不幸的是,/Zc:twoPhase/Zc:twoPhase- 選項的情況並非如此。 這些選項是「具有黏性的」,因此後續選項並無法加以覆寫。 例如:

cl /Zc:twoPhase /permissive a.cpp

在此情況下,第一個 /Zc:twoPhase 選項會啟用嚴格的雙階段名稱查閱。 第二個選項旨在停用嚴格的一致性模式 (與 /permissive- 相反),但並未停用 /Zc:twoPhase

Visual Studio 2019 16.9 版會在所有 /std 編譯器模式中變更此行為。 /Zc:twoPhase/Zc:twoPhase- 不再「具有黏性」,而稍後的選項可以加以覆寫。

解構函式範本上的明確 noexcept-specifier

編譯器先前已接受以非擲回例外狀況規格宣告,但未明確定義 noexcept-specifier 的解構函式範本。 解構函式的隱含例外狀況規格取決於類別的屬性 - 這些屬性在範本定義的時間點可能並無法知道。 C++ 標準也要求此行為:如果解構函式宣告時沒有 noexcept-specifier,則其具有隱含例外狀況規格,而且函式的其他宣告都不可以有 noexcept-specifier。

Visual Studio 2019 16.9 版會在所有 /std 編譯器模式中變更為一致的行為。

此範例顯示編譯器行為的變更:

template <typename T>
class B {
    virtual ~B() noexcept; // or throw()
};

template <typename T>
B<T>::~B() { /* ... */ } // Before: no diagnostic.
// Now diagnoses a definition mismatch. To fix, define the implementation by 
// using the same noexcept-specifier. For example,
// B<T>::~B() noexcept { /* ... */ }

C++20 中的重寫運算式

從 Visual Studio 2019 16.2 版開始,在 /std:c++latest 下,編譯器已接受類似下列範例的程式碼:

#include <compare>

struct S {
    auto operator<=>(const S&) const = default;
    operator bool() const;
};

bool f(S a, S b) {
    return a < b;
}

不過,編譯器不會叫用作者可能預期的比較函式。 上述程式碼應該將 a < b 重寫為 (a <=> b) < 0。 相反地,編譯器會使用 operator bool() 使用者定義轉換函式,並比較 bool(a) < bool(b)。 在 Visual Studio 2019 16.9 版和更新版本中,編譯器會使用預期的太空船運算子運算式來重寫運算式。

原始檔中斷性變更

正確將轉換套用至重寫的運算式有另一個效果:編譯器也會正確診斷嘗試重寫運算式所帶來的模棱兩可情況。 請考慮此範例:

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

struct Derived : Base {
    Derived();
    Derived(const Base&);
    bool operator==(const Derived& rhs) const;
};

bool b = Base{} == Derived{};

在 C++17 中,由於運算式右側 Derived 的衍生到基底轉換,因此會接受此程式碼。 在 C++20 中,也會新增合成運算式候選:Derived{} == Base{}。 由於標準中根據轉換來決定獲勝函式的規則,導致 Base::operator==Derived::operator== 之間的選擇是無法決定的。 因為兩個運算式中的轉換順序相較之下並沒有更好或更差,因此範例程式碼會產生模棱兩可的情況。

若要解決模棱兩可的問題,請新增不會受限於兩個轉換順序的新候選:

bool operator==(const Derived&, const Base&);

執行階段中斷性變更

由於 C++20 中的運算子重寫規則,因此多載解析可能會找到在較低語言模式中找不到的新候選。 而且,新候選可能比舊版的候選更好。 請考慮此範例:

struct iterator;
struct const_iterator {
  const_iterator(const iterator&);
  bool operator==(const const_iterator &ci) const;
};

struct iterator {
  bool operator==(const const_iterator &ci) const { return ci == *this; }
};

在 C++17 中,ci == *this 的唯一候選是 const_iterator::operator==。 其之所以相符,是因為 *this 會經過衍生到基底轉換,轉換成 const_iterator。 在 C++20 中,會新增另一個重寫候選:*this == ci,其會叫用 iterator::operator==。 此候選不需要轉換,因此比 const_iterator::operator== 更好。 新候選的問題在於其是目前正在定義的函式,因此函式的新語意會導致 iterator::operator== 的無限遞歸定義。

為了協助如同範例的程式碼,編譯器會實作新的警告:

$ cl /std:c++latest /c t.cpp
t.cpp
t.cpp(8): warning C5232: in C++20 this comparison calls 'bool iterator::operator ==(const const_iterator &) const' recursively

若要修正程式碼,請明確說明要使用哪一個轉換:

struct iterator {
  bool operator==(const const_iterator &ci) const { return ci == static_cast<const const_iterator&>(*this); }
};

Visual Studio 2019 16.10 版中的一致性改善

為類別複製初始化選擇錯誤的多載

以此範例程式碼為例:

struct A { template <typename T> A(T&&); };
struct B { operator A(); };
struct C : public B{};
void f(A);
f(C{});

舊版編譯器會使用 A 的範本化轉換建構函式,錯誤地將 f 的引數從類型 C 轉換成 A。 標準 C++ 要求改用轉換運算子 B::operator A。 在 Visual Studio 2019 16.10 版和更新版本中,多載解析行為會變更為使用正確的多載。

此變更也可以在某些其他情況下更正所選的多載:

struct Base 
{
    operator char *();
};

struct Derived : public Base
{
    operator bool() const;
};

void f(Derived &d)
{
    // Implicit conversion to bool previously used Derived::operator bool(), now uses Base::operator char*.
    // The Base function is preferred because operator bool() is declared 'const' and requires a qualification
    // adjustment for the implicit object parameter, while the Base function does not.
    if (d)
    {
        // ...
    }
}

浮點常值的剖析不正確

在 Visual Studio 2019 16.10 版和更新版本中,浮點常值會根據其實際類型來剖析。 舊版編譯器一律會以浮點常值具有類型 double 的形式加以剖析,然後將結果轉換成實際類型。 此行為可能會導致不正確的捨入和拒絕有效值:

// The binary representation is '0x15AE43FE' in VS2019 16.9
// The binary representation is '0x15AE43FD' in VS2019 16.10
// You can use 'static_cast<float>(7.038531E-26)' if you want the old behavior.
float f = 7.038531E-26f;

宣告點不正確

舊版編譯器無法編譯如同下列範例的自我引用程式碼:

struct S {
    S(int, const S*);

    int value() const;
};

S s(4, &s);

除非編譯器剖析包括建構函式引數的整個宣告,否則編譯器不會宣告變數 s。 建構函式引數清單中的 s 查閱將會失敗。 在 Visual Studio 2019 16.10 版和更新版本中,此範例現在會正確編譯。

不幸的是,此變更可能會中斷現有的程式碼,如下列範例所示:

S s(1, nullptr); // outer s
// ...
{
   S s(s.value(), nullptr); // inner s
}

在舊版編譯器中,當其在建構函式引數中查閱 s,以尋找 s 的「內部」宣告時,其會尋找先前的宣告 (「外部」s),然後程式碼會編譯。 從 16.10 版開始,編譯器會改為發出警告 C4700 (部分機器翻譯)。 這是因為編譯器現在會在剖析建構函式引數之前宣告「內部」s。 因此,s 查閱會找到尚未初始化的「內部」s

類別範本的明確特製化成員

舊版編譯器會錯誤地將類別範本成員的明確特製化標示為 inline,如果其也已在主要範本中定義的話。 此行為表示編譯器有時會拒絕一致的程式碼。 在 Visual Studio 2019 16.10 版和更新版本中,明確特製化在 /permissive- 模式中已不會再以隱含方式標示為 inline。 請考慮此範例:

來源檔案 s.h

// s.h
template<typename T>
struct S {
    int f() { return 1; }
};
template<> int S<int>::f() { return 2; }

來源檔案 s.cpp

// s.cpp
#include "s.h"

來源檔案 main.cpp

// main.cpp
#include "s.h"

int main()
{
}

若要解決上述範例中的連結器錯誤,請明確地將 inline 新增至 S<int>::f

template<> inline int S<int>::f() { return 2; }

推斷的傳回類型名稱修飾

在 Visual Studio 2019 16.10 版和更新版本中,編譯器會變更其針對具有已推斷傳回類型之函式修飾名稱的產生方式。 例如,以下列函式為例:

auto f() { return 0; }
auto g() { []{}; return 0; }

舊版編譯器會針對連結器產生這些名稱:

f: ?f@@YAHXZ -> int __cdecl f(void)
g: ?g@@YA@XZ -> __cdecl g(void)

令人驚訝的是,因為函式主體中本機 Lambda 所造成的其他語意行為,因此會從 g 中省略傳回類型。 這種不一致使得難以實作具有已推斷傳回類型的匯出函式:模組介面需要函式主體編譯方式的相關資訊。 其需要該資訊,才能在匯入端產生可正確連結至定義的函式。

編譯器現在會省略已推斷傳回類型函式的傳回類型。 此行為與其他主要實作一致。 函式範本有一個例外:此版本的編譯器會針對具有已推斷傳回類型的函式範本引進新的修飾名稱行為:

template <typename T>
auto f(T) { return 1; }

template <typename T>
decltype(auto) g(T) { return 1.; }

int (*fp1)(int) = &f;
double (*fp2)(int) = &g;

autodecltype(auto) 的修飾名稱現在會以二進位的形式出現,而不是已推斷的傳回類型:

f: ??$f@H@@YA?A_PH@Z -> auto __cdecl f<int>(int)
g: ??$g@H@@YA?A_TH@Z -> decltype(auto) __cdecl g<int>(int)

舊版編譯器會包括推斷的傳回類型作為簽章的一部分。 當編譯器在修飾名稱中包括傳回類型時,可能會導致連結器問題。 有些其他格式良好的案例,對於連結器而言可能會變得模棱兩可。

新的編譯器行為可能會產生二進位中斷性變更。 請考慮此範例:

來源檔案 a.cpp

// a.cpp
auto f() { return 1; }

來源檔案 main.cpp

// main.cpp
int f();
int main() { f(); }

在 16.10 版之前的版本中,編譯器會針對 auto f() 產生名稱,其看起來像是 int f(),即使其為語意上不同的函式。 這表示範例會編譯。 若要修正此問題,請勿依賴 f 的原始定義中的 auto。 請改為將其撰寫為 int f()。 由於具有已推斷傳回類型的函式一律會編譯,因此會將 ABI 的影響最小化。

對已忽略 nodiscard 屬性的警告

舊版編譯器會以無訊息方式忽略 nodiscard 屬性的某些使用。 如果屬性位於未套用至所宣告之函式或類別的語法位置,則會忽略屬性。 例如:

static [[nodiscard]] int f() { return 1; }

在 Visual Studio 2019 16.10 版和更新版本中,編譯器會改為發出層級 4 警告 C5240:

a.cpp(1): warning C5240: 'nodiscard': attribute is ignored in this syntactic position

若要修正此問題,請將屬性移至正確的語法位置:

[[nodiscard]] static int f() { return 1; }

針對在模組 purview 中具有系統標頭名稱 include 指示詞的警告

在 Visual Studio 2019 16.10 版和更新版本中,編譯器會發出警告以防止常見的模組介面製作錯誤。 如果您在 export module 陳述式之後包括標準程式庫標頭,編譯器會發出警告 C5244。 以下是範例:

export module m;
#include <vector>

export
void f(std::vector<int>);

開發人員可能不想讓模組 m 擁有 <vector> 的內容。 編譯器現在會發出警告,以協助尋找並修正問題:

m.ixx(2): warning C5244: '#include <vector>' in the purview of module 'm' appears erroneous. Consider moving that directive before the module declaration, or replace the textual inclusion with an "import <vector>;".
m.ixx(1): note: see module 'm' declaration

若要修正此問題,請將 #include <vector> 移到 export module m; 之前:

#include <vector>
export module m;

export
void f(std::vector<int>);

針對未使用的內部連結函式的警告

在 Visual Studio 2019 16.10 版和更新版本中,編譯器會在已移除具有內部連結的未參考函式的更多情況下發出警告。 舊版編譯器會針對下列程式碼發出警告 C4505 (部分機器翻譯):

static void f() // warning C4505: 'f': unreferenced function with internal linkage has been removed
{
}

編譯器現在也會針對未參考的 auto 函式和位於匿名命名空間中的未參考函式發出警告。 其會針對下列兩個函式發出預設關閉 (部分機器翻譯) 的警告 C5245:

namespace
{
    void f1() // warning C5245: '`anonymous-namespace'::f1': unreferenced function with internal linkage has been removed
    {
    }
}

auto f2() // warning C5245: 'f2': unreferenced function with internal linkage has been removed
{
    return []{ return 13; };
}

對括號省略的警告

在 Visual Studio 2019 16.10 版和更新版本中,編譯器會對未針對子物件使用括號的初始化清單發出警告。 編譯器會發出預設關閉 (部分機器翻譯) 的警告 C5246。

以下是範例:

struct S1 {
  int i, j;
};

struct S2 {
   S1 s1;
   int k;
};

S2 s2{ 1, 2, 3 }; // warning C5246: 'S2::s1': the initialization of a subobject should be wrapped in braces

若要修正此問題,請將子物件的初始化包裝在括號之中:

S2 s2{ { 1, 2 }, 3 };

正確偵測 const 物件是否未初始化

在 Visual Studio 2019 16.10 版和更新版本中,編譯器現在會在您嘗試定義未完全初始化的 const 物件時發出錯誤 C2737:

struct S {
   int i;
   int j = 2;
};

const S s; // error C2737: 's': const object must be initialized

舊版編譯器會允許此程式碼編譯,即使 S::i 未初始化。

若要修正此問題,請在建立物件的 const 執行個體之前將所有成員初始化:

struct S {
   int i = 1;
   int j = 2;
};

Visual Studio 2019 16.11 版中的一致性改善

/std:c++20 編譯器模式

在 Visual Studio 2019 16.11 版和更新版本中,編譯器現在支援 /std:c++20 編譯器模式。 先前,C++20 功能只能在 Visual Studio 2019 的 /std:c++latest 模式中使用。 原本需要 /std:c++latest 模式的 C++20 功能現在可在最新版 Visual Studio 的 /std:c++20 模式或更新版本中運作。

另請參閱

Microsoft C/C++ 語言一致性