Visual Studio 2019의 C++ 규칙 향상, 동작 변경 및 버그 수정

Visual Studio의 Microsoft C/C++(MSVC)는 모든 릴리스에서 규칙을 개선하고 버그를 수정합니다. 이 문서에는 주 릴리스와 버전별 개선 사항이 나와 있습니다. 특정 버전의 변경 내용으로 바로 이동하려면 이 문서의 내용 아래의 목록을 사용하세요.

이 문서에는 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에는 다음과 같은 Microsoft C++ 컴파일러의 규칙 향상, 버그 수정 및 동작 변경이 포함되어 있습니다.

참고

C++20 기능은 C++20 구현이 완료된 것으로 간주될 때까지 Visual Studio 2019의 /std:c++latest 모드에서만 사용할 수 있었습니다. 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에는 향상된 지원이 추가되었습니다. 자세한 내용은 MSVC 2017 버전 15.9를 사용하여 C++ 모듈의 템플릿 지원 및 오류 검색 향상을 참조하세요.

집계 유형의 수정된 사양

집계 유형의 사양이 C++20에서 변경되었습니다(사용자가 선언한 생성자로 집계 금지 참조). 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에서는 "우주선 연산자"라고도 하는 <=> 3방향 비교 연산자를 소개합니다. Visual Studio 2019 버전 16.0의 /std:c++latest 모드는 현재 허용되지 않는 구문 오류를 발생시켜 연산자를 부분적으로 지원합니다. 예를 들어 다음 코드는 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;
}

람다 클로저

C++14에서 람다 클로저 형식은 리터럴이 아닙니다. 이 규칙의 기본 결과는 람다가 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 형식 오류가 success-but-directory-not-created 코드로 변경되었습니다.

operator<<(std::ostream, nullptr_t)

LWG 2221에 따라, 스트림에 nullptr 를 쓰기 위해 operator<<(std::ostream, nullptr_t)를 추가했습니다.

병렬 알고리즘 추가

is_sorted, is_sorted_until, is_partitioned, set_difference, set_intersection, is_heapis_heap_until의 새 병렬 버전.

원자성 초기화 수정 사항

P0883 ‘원자성 초기화 수정’std::atomic을 변경하여 포함된 T를 기본 초기화하는 대신 값을 초기화합니다. 이 수정 내용은 Microsoft 표준 라이브러리에서 Clang/LLVM을 사용할 때 활성화됩니다. constexpr 처리 버그를 해결하기 위해 현재 Microsoft C++ 컴파일러에는 사용할 수 없습니다.

remove_cvrefremove_cvref_t

P0550에서 remove_cvrefremove_cvref_t 형식 특성을 구현했습니다. 이러한 특성은 함수 및 배열이 손상되지 않는 유형에서 포인터(std::decaystd::decay_t와는 달리)로 참조 및 cv 한정자를 제거합니다.

기능 테스트 매크로

P0941R2 - feature-test 매크로__has_cpp_attribute에 대한 지원으로 완료되었습니다. 기능 테스트 매크로는 모든 표준 모드에서 지원됩니다.

사용자 선언 생성자를 사용하여 집계 금지

C++20 P1008R1 - 사용자 선언 생성자를 사용하여 집계 금지가 완료되었습니다.

constexpr 함수의 reinterpret_cast

constexpr 함수에서 reinterpret_cast 가 잘못되었습니다. 이전에는 Microsoft C++ 컴파일러가 constexpr 컨텍스트에서 사용된 reinterpret_cast 만 거부했습니다. 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를 사용하여 컴파일러 진단을 표시하지 않습니다. 다음 코드는 out을 초기화할 때 wchar_t에서 char로 데이터가 손실될 수 있음에도 불구하고 Visual Studio 2017에서 경고 없이 컴파일됩니다.

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에서는 또는 /ZW 아래의 잘못된 +=/clr-= 호출에 대해 컴파일러가 자동으로 오류를 무시하고 코드를 생성하지 않는 버그가 도입되었습니다. 다음 코드는 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에서 오류 없이 컴파일되지만 Visual Studio 2019의 /std:c++17 모드 이상에서는 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에서는 설명을 포함하는 C4165와 함께 신중하게 조정된 C4800이 다시 추가되었습니다. 두 경고 모두 명시적 캐스트를 사용하거나 적절한 형식의 0 비교를 통해 표시되지 않도록 쉽게 설정할 수 있습니다. C4800은 off-by-default 수준 4 경고이고 C4165는 off-by-default 수준 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에서 경고 C4822는 컴파일러 옵션 /w14822가 명시적으로 설정된 경우에만 발생합니다. /Wall로 표시되지는 않습니다. Visual Studio 2019에서 C4822는 /w14822 를 명시적으로 설정하지 않고도 /Wall 에서 검색할 수 있도록 하는 off-by-default 경고입니다.

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

if constexpr 문을 포함하는 함수 템플릿 본문

Visual Studio 2019의 /std:c++20 또는 /std:c++latest 에서, 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";
}

이 오류를 방지하려면 a 선언 typename T::Type a;typename 키워드를 추가합니다.

인라인 어셈블리 코드는 람다 식에서 지원되지 않음

Microsoft C++ 팀은 최근 람다 내에서 인라인 어셈블러를 사용하면 런타임 시 ebp(반환 주소 레지스터)가 손상될 수 있는 보안 문제를 인식하게 되었습니다. 악의적인 공격자가 이 시나리오를 활용할 수 있었습니다. 인라인 어셈블러는 x86에서만 지원되며, 인라인 어셈블러와 컴파일러 나머지 부분 간의 상호 작용은 좋지 않습니다. 이러한 사실과 문제의 특성을 감안할 때 이 문제의 가장 안전한 해결책은 람다 식 내에서 인라인 어셈블러를 허용하지 않는 것입니다.

발견된 ‘실제’ 사례에서 인라인 어셈블러가 람다 식 내에서 사용되는 유일한 목적은 반환 주소를 캡처하기 위한 것이었습니다. 이 시나리오에서는 컴파일러 고유의 _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_type, std::allocator::difference_type은 더 이상 사용되지 않습니다.

문자열 변환을 좁히기 위한 올바른 경고

표준이 요청하지 않았고 실수로 C4244 축소 경고가 표시되지 않도록 한 의사 static_caststd::string에서 제거되었습니다. 이제 std::string::string(const wchar_t*, const wchar_t*)를 호출하려 시도하면 wchar_tchar로 축소에 대한 C4244가 올바르게 생성됩니다.

다양한 <filesystem> 정확성 수정

  • 디렉터리의 마지막 쓰기 시간을 변경하려고 할 때 std::filesystem::last_write_time이 실패하는 오류가 수정되었습니다.
  • 존재하지 않는 대상 경로를 제공할 때 std::filesystem::directory_entry 생성자는 이제 예외를 throw하는 대신 실패한 결과를 저장합니다.
  • existing_p가 symlink인 경우 기본 CreateDirectoryExW 함수가 copy_symlink를 사용하므로 std::filesystem::create_directory 2-매개 변수 버전이 1-매개 변수 버전을 호출하도록 변경되었습니다.
  • 손상된 symlink가 발견될 때 std::filesystem::directory_iterator에서 더 이상 오류가 발생하지 않습니다.
  • std::filesystem::space는 이제 상대 경로를 허용합니다.
  • std::filesystem::path::lexically_relativeLWG 3096으로 보고된 후행 슬래시로 더 이상 혼동하지 않습니다.
  • std::filesystem::create_symlink에서 슬래시가 있는 경로를 거부하는 CreateSymbolicLinkW를 해결했습니다.
  • Windows 10 LTSB 1609에 존재하지만 실제로 파일을 삭제할 수 없는 POSIX 삭제 모드 delete 함수가 해결되었습니다.
  • std::boyer_moore_searcherstd::boyer_moore_horspool_searcher 복사 생성자 및 복사 할당 연산자가 이제 실제로 항목을 복사합니다.

Windows 8 이상의 병렬 알고리즘

병렬 알고리즘 라이브러리는 이제 Windows 7 및 이전 가짜 버전을 항상 사용하는 대신 Windows 8 이상에서 실제 WaitOnAddress 제품군을 올바르게 사용합니다.

std::system_category::message() 공백

std::system_category::message()는 이제 반환된 메시지에서 후행 공백을 삭제합니다.

std::linear_congruential_engine 0으로 나누기

std::linear_congruential_engine이 0으로 나누도록 트리거하는 일부 조건이 수정되었습니다.

반복기 래핑 해제를 위한 수정

일부 반복기 래핑 해제 동작은 Visual Studio 2017 15.8에서 프로그래머와 사용자 통합을 위해 처음 공개되었습니다. 이 내용은 C++ 팀 블로그 문서 VS 2017 15.8의 STL 기능 및 수정에서 설명했습니다. 이 동작은 표준 라이브러리 반복기에서 파생된 반복기를 더 이상 래핑 해제하지 않습니다. 예를 들어 std::vector<int>::iterator에서 파생되고 동작을 사용자 지정하려는 사용자는 이제 포인터의 동작 대신 표준 라이브러리 알고리즘을 호출할 때 사용자 지정된 동작을 가져옵니다.

순서가 지정되지 않은 컨테이너 reserve 함수가 이제 LWG 2156에 설명된 대로 N개 요소를 실제로 예약합니다.

시간 처리

  • 이전에는 동시성 라이브러리에 전달된 일부 시간 값이 오버플로되었습니다(예: condition_variable::wait_for(seconds::max())). 현재 수정된 이 오버플로는 임의의 29일 주기로 동작을 변경했습니다(기본 Win32 API에서 허용된 uint32_t 밀리초가 오버플로된 경우).

  • <ctime> 헤더는 이제 timespectimespec_get을 네임스페이스 std에서 올바르게 선언하고 전역 네임스페이스에서도 선언합니다.

컨테이너에 대한 다양한 수정

  • 향상된 IntelliSense 환경을 위해 private으로 설정된 표준 라이브러리 내부 컨테이너 함수가 많습니다. MSVC의 이후 릴리스에서는 멤버를 private로 표시하기 위한 추가 수정이 필요한 상황입니다.

  • list, map, unordered_map 등의 노드 기반 컨테이너가 손상될 수 있는 예외 안전성 문제를 해결했습니다. propagate_on_container_copy_assignment 또는 propagate_on_container_move_assignment 재할당 작업 중에, 컨테이너의 sentinel 노드를 이전 할당자로 해제하고 이전 할당자를 통해 POCCA/POCMA 할당을 수행한 다음, 새 할당자에서 sentinel 노드를 가져오려고 시도합니다. 이 할당이 실패한 경우 컨테이너가 손상된 것입니다. sentinel 노드는 소유가 하드 데이터 구조 고정이므로 제거할 수도 없었습니다. 이 코드는 기존 sentinel 노드를 제거하기 전에 소스 컨테이너의 할당자를 사용하여 새 sentinel 노드를 할당하도록 수정되었습니다.

  • 컨테이너는 is_always_equal로 선언된 할당자에 대해서도 propagate_on_container_copy_assignment, propagate_on_container_move_assignmentpropagate_on_container_swap에 따라 할당자를 항상 복사/이동/스왑하도록 수정되었습니다.

  • rvalue 컨테이너를 수락하는 컨테이너 병합 및 추출 멤버 함수에 대한 오버 로드가 추가되었습니다. 자세한 내용은 P0083 "맵 및 집합 스플라이스"를 참조하세요.

\r\n`` =>\n`의 std::basic_istream::read 처리

std::basic_istream::read\r\n\n으로 처리하는 과정의 일부로서 일시적으로 제공된 버퍼의 파트에 쓰지 않도록 수정되었습니다. 이 변경으로 인해 Visual Studio 2017 15.8에서 4K보다 큰 읽기에 대해 얻은 성능 향상이 일부 손실됩니다. 하지만 문자당 3회의 가상 호출 방지를 통한 효율성 향상은 유지됩니다.

std::bitset 생성자

std::bitset 생성자는 큰 비트 세트에 대해 더 이상 1과 0을 역순으로 읽지 않습니다.

std::pair::operator= 재발

LWG 2729 "Missing SFINAE on std::pair::operator=";를 구현할 때 도입된 std::pair의 할당 연산자의 회귀를 수정했습니다. 이제 std::pair로 변환할 수 있는 형식을 다시 올바르게 허용합니다.

add_const_t에 대해 추론되지 않은 컨텍스트

add_const_t 및 관련 함수가 추론되지 않은 컨텍스트로 간주되는 부 형식 특성 버그를 수정했습니다. 즉, add_const_tconst T가 아니라 typename add_const<T>::type의 별칭이어야 합니다.

16.1의 규칙 향상

char8_t

P0482r6. C++20은 UTF-8 코드 단위를 나타내는 데 사용되는 새로운 문자 형식을 추가합니다. C++20의 u8 문자열 리터럴에는 이전의 const char[N] 대신 const char8_t[N] 형식이 있습니다. N2231에서 C 표준에 대해 유사한 변경이 제안되었습니다. char8_t 이전 버전과의 호환성 수정에 대한 제안은 P1423r3에서 제공됩니다. 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 메타 함수 및 std::identity 함수 개체로 대체되었습니다. 둘 다 /std:c++latest(Visual Studio 2019 버전 16.11 이상에서는 /std:c++20)에서만 사용할 수 있습니다.

다음 예제에서는 Visual Studio 2017에서 std::identity(<type_traits>에서 정의됨)에 대한 사용 중단 경고 C4996을 생성합니다.

#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(<functional>에서 정의됨)를 std::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);

일반 람다에 대한 구문 검사

/std:c++latest(Visual Studio 2019 버전 16.11 이상에서는 /std:c++20) 아래에서 또는 Visual Studio 2019 버전 16.9 이상(이전에는 Visual Studio 2019 버전 16.3부터 /experimental:newLambdaProcessor으로 사용 가능)에서 /Zc: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_view에 대한 starts_with()ends_with().
  • 연관 컨테이너에 대한 contains().
  • listforward_list에 대한 remove(), remove_if()unique()가 이제 size_type을 반환합니다.
  • shift_left()shift_right()가 <algorithm>에 추가되었습니다.

16.2의 규칙 향상

noexceptconstexpr 함수

constexpr 함수는 상수 식에서 사용될 때 더 이상 기본적으로 noexcept 로 간주되지 않습니다. 이 동작 변경은 CWG(Core Working Group) 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 컴파일러 옵션을 사용하도록 설정한 경우(Visual Studio 2019 버전 16.11 이상에서는 /std:c++20) 다음 코드는 수준 4 경고 C5054를 생성합니다.

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 컴파일러 옵션을 사용하도록 설정한 경우(Visual Studio 2019 버전 16.11 이상에서는 /std:c++20) 다음 코드는 수준 1 경고 C5056을 생성합니다.

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: endian
  • P0482R6: char8_t에 대한 라이브러리 지원
  • P0600R1: STL, 파트 1에 대한 [[nodiscard]]
  • P0653R2: to_address()
  • P0754R2: <version>
  • P0771R1: std::function의 이동 생성자에 대한 noexcept

연관 컨테이너에 대한 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를 발생시킵니다.

생성자를 형식 이름으로 사용할 수 없음

생성자 이름이 클래스 템플릿 특수화에 대한 별칭 뒤의 정규화된 이름에 표시되는 경우 컴파일러는 더 이상 생성자 이름을 삽입된 클래스 이름으로 간주하지 않습니다. 이전에는 생성자를 다른 엔터티를 선언하는 형식 이름으로 사용할 수 있었습니다. 다음 예제에서는 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-의 정규화된 ID에 대한 한층 개선된 2단계 이름 조회

2단계 이름 조회를 위해서는 템플릿 본문에 사용된 종속되지 않은 이름이 정의 시점에 템플릿에 표시될 수 있어야 합니다. 이전에는 템플릿이 인스턴스화될 때 이러한 이름이 발견되었을 수 있습니다. 이번 변경을 통해 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 컴파일러는 이제 규칙 모드(/permissive-)에서 CWG 이슈 903을 구현합니다. 이 규칙은 정수 계열 상수 식에서 Null 포인터 상수로의 암시적 변환(정수 리터럴 '0'은 제외)을 허용하지 않습니다. 다음 예에서는 규칙 모드에서 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 *'
}

오류를 해결하려면 false 대신 nullptr 을 사용합니다. 리터럴 0은 다음과 같이 계속 허용됩니다.

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

정수 리터럴 형식에 대한 표준 규칙

규칙 모드(/permissive-로 사용 설정됨)에서는 MSVC에 정수 리터럴 형식에 대한 표준 규칙이 사용됩니다. 이전에는 너무 커서 signed int 에 맞지 않는 10진 리터럴에 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");

컴파일러에서 제공되는 비교 연산자에 대한 변경 내용

/std:c++20 또는 /std:c++latest 옵션을 사용하도록 설정한 경우 MSVC 컴파일러는 이제 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의 규칙 개선

이니셜라이저가 없는 명시적 특수화 선언은 정의가 아님

MSVC는 이제 /permissive- 아래에서 이니셜라이저가 없는 명시적 특수화 선언은 정의가 아니라는 표준 규칙을 적용합니다. 이전에는 이러한 선언이 기본 이니셜라이저를 갖는 정의로 간주되었습니다. 이 동작을 사용하는 프로그램은 이제 확인되지 않은 기호를 갖게 되므로 변경의 효과는 링크 타임에 볼 수 있습니다. 다음 예는 이제 오류를 발생시킵니다.

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를 동일한 줄에 유지합니다.

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_tstd::ostream에 삽입하고, char16_t 또는 char32_tstd::ostream 또는 std::wostream에 삽입하면 정수 계열 값이 출력됩니다. 이러한 문자 형식에 포인터를 삽입하면 포인터 값이 출력됩니다. 프로그래머에게는 두 가지 경우 어느 것도 직관적이지 않습니다. 프로그래머는 흔히 표준 라이브러리가 문자 또는 null로 끝나는 문자열을 대신 트랜스코딩하고 결과를 출력할 것으로 예상합니다.

C++20 제안 P1423R3은 스트림 및 문자 또는 문자 포인터 형식의 조합에 대해 삭제된 이러한 스트림 삽입 연산자 오버로드를 추가합니다. /std:c++20 또는 /std:c++latest 에서 오버로드는 의도하지 않은 방식으로 동작하는 대신 이러한 삽입을 잘못된 형식으로 만듭니다. 컴파일러는 이러한 삽입이 발견되면 오류 C2280을 발생시킵니다. "이스케이프 해치" 매크로 _HAS_STREAM_INSERTION_OPERATORS_DELETED_IN_CXX201로 정의하여 이전 동작을 복원할 수 있습니다. (이 제안은 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로 또는 pointer-to-character 형식을 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::pow()의 변경된 반환 형식 std::complex

이전에는 함수 템플릿 std::pow()의 반환 형식에 대한 승격 규칙의 MSVC 구현이 잘못되었습니다. 예를 들어 이전에는 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 로 변경합니다. pow에서 반환된 형식과 일치하도록 이 인수를 complex<double>로 직접 변환할 수 있습니다.

  • complex<float>{pow(ARG, ARG)}를 명시하여 pow의 결과 범위를 complex<float>로 좁힙니다. 그런 다음 float 값을 계속 곱할 수 있습니다.

  • int 대신 floatpow에 전달합니다. 이 연산은 느릴 수 있습니다.

  • 경우에 따라 pow를 완전히 피할 수 있습니다. 예를 들어 pow(cf, -1)을 나누기로 대체할 수 있습니다.

C에 대한 switch 경고

Visual Studio 2019 버전 16.6 이상에서 컴파일러는 C로 컴파일된 코드에 대한 일부 기존 C++ 경고를 구현합니다. 이제 다음과 같은 경고를 다양한 수준에서 사용 설정할 수 있습니다. C4060, C4061, C4062, C4063, C4064, C4065, C4808 및 C4809. C4065 및 C4060 경고는 C에서 기본적으로 사용되지 않습니다.

경고는 누락된 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에서 기본 인수 가져오기

.NET Core에 기본 인수가 있는 API 수가 늘어나고 있습니다. 따라서 이제 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의 규칙 개선

일반적으로 복사 가능 정의

C++20에서 일반적으로 복사 가능의 정의가 변경되었습니다. 클래스에 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 버전에서 컴파일되지 않습니다. 이 변경 내용을 검색하는 데 사용할 수 있는 off-by-default 컴파일러 경고가 있습니다. 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는 구현 내용에서 이전에 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에서 nullptrdirect-conversion으로서 bool로만 변환할 수 있습니다. 예를 들어, 중괄호로 묶인 이니셜라이저 목록을 사용하여 bool을 초기화하는 경우입니다. 이 제한은 MSVC에서 적용되지 않았습니다. 이제 MSVC는 /permissive-에서 이 규칙을 구현합니다. 이제 암시적 변환이 잘못된 형식으로 진단됩니다. 직접 초기화 bool b(nullptr)가 유효하기 때문에 bool 에 대한 컨텍스트 변환은 계속 허용됩니다.

대부분의 경우 이 오류는 다음 예제와 같이 nullptrfalse 로 바꿔 해결할 수 있습니다.

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
}

오버로드된 이름을 갖는 클래스 멤버의 초기화가 올바르게 시퀀싱됨

형식 이름이 데이터 멤버의 이름으로도 오버로드되는 경우 클래스 데이터 멤버의 내부 표현에서 버그가 확인되었습니다. 이 버그로 인해 집계 초기화 및 멤버 초기화 순서가 일관적이지 않습니다. 이제 생성된 초기화 코드가 올바릅니다. 그러나 이 변경으로 인해 다음 예제와 같이 잘못 정렬된 멤버를 실수로 사용한 소스에서 오류 또는 경고가 발생할 수 있습니다.

// 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
};

이전 버전에서 생성자가 데이터 멤버 Inner를 데이터 멤버 v보다 먼저 잘못 초기화합니다. (C++ 표준에서는 멤버의 선언 순서와 동일한 초기화 순서를 요구합니다.) 이제 생성된 코드가 표준을 따르므로 멤버 초기화 목록은 순서가 잘못되었습니다. 컴파일러는 이 예제에서 경고를 발생시킵니다. 이 문제를 해결하려면 선언 순서를 반영하도록 멤버 이니셜라이저 목록을 다시 정렬합니다.

정수 오버로드 및 long 인수를 포함하는 오버로드 확인

C++ 표준에서는 long 에서 int 로 변환을 표준 변환으로 순위를 지정해야 합니다. 이전 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;
};

이 동작 변경은 /std:c++20 또는 /std:c++latest 뿐 아니라 MSVC의 모든 C++ 언어 모드에 적용됩니다.

축소 변환이 보다 일관적으로 진단됨

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의 규칙 개선

'rvalue 클래스를 lvalue로 사용' 확장

MSVC에는 rvalue 클래스를 lvalue로 사용할 수 있도록 하는 확장이 있습니다. 확장은 클래스 rvalue의 수명을 연장하지 않으며, 런타임에 정의되지 않은 동작이 발생할 수 있습니다. 이제는 표준 규칙을 적용하고 /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 동작이 변경됩니다. 이 static_assert 는 C++17 모드에서는 실패하지만 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가 함수 템플릿이므로 컴파일러는 함수 인수를 추론되지 않은 컨텍스트로 처리해야 합니다. 즉, 이 경우 컴파일러가 템플릿 매개 변수 T의 형식을 추론할 수 없으므로 g에 대한 호출이 실패합니다. 불행히도 컴파일러는 ::f가 함수 호출과 잘 일치한다고 이미 결정했다는 사실을 취소하지 못했습니다. 컴파일러는 오류를 내보내는 대신 인수로 ::f를 사용하여 g를 호출하는 코드를 생성합니다.

대부분의 경우 사용자는 함수 인수로 ::f를 사용하는 것을 예상하기 때문에 /permissive- 를 사용하여 코드가 컴파일되는 경우에만 오류를 내보냅니다.

/await에서 C++ 20 코루틴으로 마이그레이션

표준 C++20 코루틴은 이제 /std:c++20/std:c++latest 에서 기본적으로 설정되어 있습니다. 이 코루틴은 코루틴 TS 및 /await 옵션에서의 지원과는 다릅니다. /await 에서 표준 코루틴으로 마이그레이션하려면 일부 원본 변경이 필요할 수 있습니다.

비표준 키워드

이전 awaityield 키워드는 C++20 모드에서 지원되지 않습니다. 코드에서 co_awaitco_yield 를 대신 사용해야 합니다. 표준 모드는 코루틴에서 return 사용도 허용하지 않습니다. 코루틴의 모든 returnco_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 에서 프라미스 initial 및 suspend 함수는 bool 을 반환하는 것으로 선언될 수 있습니다. 이 동작은 표준이 아닙니다. C++20에서 이러한 함수는 대기 가능 클래스 형식을 반환해야 합니다. 이는 종종 트리비얼 대기 가능 형식 중 하나로, 함수가 이전에 true 를 반환한 경우에는 std::suspend_always, 함수가 false 를 반환한 경우에는 std::suspend_never입니다.

// /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에서 프라미스 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 는 예외 처리 함수나 std::exception_ptr을 사용하는 set_exception이라는 예외 처리 함수를 사용하여 프라미스 형식을 지원합니다. C++20에서 프라미스 형식에는 인수를 사용하지 않는 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의 반환 형식

프라미스 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
};

반환 개체 변환 동작

코루틴의 선언된 반환 형식이 프라미스 get_return_object 함수의 반환 형식과 일치하지 않는 경우 get_return_object에서 반환된 개체가 코루틴의 반환 형식으로 변환됩니다. /await 에서 이 변환은 코루틴 본문을 실행하기 전에 조기에 수행됩니다. /std:c++20 또는 /std:c++latest 에서 이 변환은 값이 호출자에게 반환될 때 수행됩니다. 이를 통해 초기 일시 중단 지점에서 일시 중단되지 않는 코루틴이 코루틴 본문 내에서 get_return_object가 반환하는 개체를 사용할 수 있습니다.

코루틴 프라미스 매개 변수

C++20에서 컴파일러는 코루틴 매개 변수(있는 경우)를 프라미스 형식의 생성자에 전달하려 시도합니다. 실패하면 기본 생성자를 사용하여 다시 시도합니다. /await 모드에서는 기본 생성자만 사용되었습니다. 이러한 변경으로 인해 프라미스에 여러 생성자가 있는 경우, 또는 코루틴 매개 변수에서 프라미스 형식으로 변환하는 경우 동작이 달라질 수 있습니다.

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{});

/std:c++20에서 기본적으로 설정된 /permissive- 및 C++20 모듈

C++20 모듈 지원은 /std:c++20/std:c++latest 에서 기본적으로 설정되어 있습니다. 이러한 변경에 대한 자세한 내용과 moduleimport가 키워드로 조건부 처리되는 시나리오는 Visual Studio 2019 버전 16.8에서 MSVC로 표준 C++20 모듈 지원을 참조하세요.

이제 /std:c++20 또는 /std:c++latest 가 지정되면 모듈 지원의 필수 구성 요소로 permissive- 가 사용됩니다. 자세한 내용은 /permissive-를 참조하세요.

이전에 /std:c++latest 에서 컴파일하고 비준수 컴파일러 동작 /permissive 가 필요한 코드의 경우 컴파일러에서 엄격한 규칙 모드를 해제하도록 지정할 수 있습니다. 컴파일러 옵션은 명령줄 인수 목록에서 /std:c++latest 뒤에 나타나야 합니다. 그러나 모듈 사용이 검색되는 경우 /permissive 로 인해 오류가 발생합니다.

오류 C1214: 모듈이 '옵션'을 통해 요청된 비표준 동작과 충돌합니다.

옵션의 가장 일반적인 값은 다음과 같습니다.

옵션 Description
/Zc:twoPhase- 2단계 이름 조회는 C++20 모듈에 필요하며, /permissive- 에 포함됩니다.
/Zc:hiddenFriend- 표준 숨겨진 friend 이름 조회 규칙은 C++20 모듈에 필요하고 /permissive- 에 포함됩니다.
/Zc:lambda- 표준 람다 처리는 C++20 모듈에 필요하며 /std:c++20 모드 이상에 포함됩니다.
/Zc:preprocessor- 규격 전처리기는 C++20 헤더 단위 사용 및 생성에만 필요합니다. 명명된 모듈에는 이 옵션이 필요하지 않습니다.

아직 표준화되지 않았기 때문에 Visual Studio와 함께 제공되는 std.* 모듈을 사용하려면 /experimental:module 옵션이 여전히 필요합니다.

/experimental:module 옵션은 /Zc:twoPhase, /Zc:lambda, /Zc:hiddenFriend도 포함합니다. 이전에는 모듈로 컴파일된 코드는 모듈만 사용된 경우 때때로 /Zc:twoPhase- 로 컴파일할 수 있었습니다. 이 동작은 더 이상 지원되지 않습니다.

Visual Studio 2019 버전 16.9의 규칙 개선

참조 직접 초기화에서 임시 개체 복사 초기화

Core Working Group 문제 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
}

소멸자 특성 및 생성될 수 있는 하위 개체

Core Working Group 문제 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::~Dnoexcept(false)가 되도록 확인되어 해당 기본 클래스보다 약한 예외 사양을 갖는 파생 클래스가 생성됩니다. 이 해석은 안전하지 않습니다. B에서 파생된 클래스의 소멸자에서 예외가 throw되는 경우 잘못된 런타임 동작이 발생할 수 있습니다.

이 변경에 따라 소멸자에 가상 소멸자가 있고 가상 기본 클래스에 잠재적으로 throw하는 소멸자가 있는 경우 해당 소멸자도 잠재적으로 throw할 수 있습니다.

유사한 형식 및 참조 바인딩

Core Working Group 문제 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 옵션은 엄격한 2단계 이름 조회를 사용하도록 설정합니다. 두 번째 옵션은 엄격한 규칙 모드( /permissive- 의 반대)를 사용하지 않도록 설정하지만, /Zc:twoPhase 를 사용하지 않도록 설정하지는 않습니다.

Visual Studio 2019 버전 16.9는 모든 /std 컴파일러 모드에서 이 동작을 변경합니다. /Zc:twoPhase/Zc:twoPhase- 는 더 이상 ‘고정’되지 않고 후속 옵션으로 재정의할 수 있습니다.

소멸자 템플릿의 명시적 noexcept 지정자

이전에는 컴파일러가 throw하지 않는 예외 사양을 사용하여 선언되었지만 명시적 noexcept 지정자 없이 정의된 소멸자 템플릿을 허용했습니다. 소멸자의 암시적 예외 사양은 클래스의 속성(템플릿의 정의 시점에서 알려지지 않을 수 있는 속성)에 따라 달라집니다. C++ 표준에서는 다음 동작도 필요합니다. 소멸자가 noexcept 지정자를 사용하지 않고 선언된 경우, 해당 소멸자는 암시적 예외 사양을 사용하고 다른 함수 선언은 noexcept 지정자를 가질 수 없습니다.

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 이상에서 컴파일러는 예상되는 spaceship 연산자 식을 사용하여 식을 다시 작성합니다.

소스 주요 변경 내용

다시 작성된 식에 변환을 제대로 적용하면 컴파일러가 식을 다시 작성하려는 시도에서 모호성을 올바르게 진단하는 또 하나의 효과가 있습니다. 다음 예제를 고려해 보세요.

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의 템플릿 변환 생성자를 사용하여 C 형식의 f 인수에서 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()
{
}

위의 예제에서 링커 오류를 해결하려면 inlineS<int>::f에 명시적으로 추가합니다.

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

추론된 반환 형식 이름 꾸미기

Visual Studio 2019 버전 16.10 이상에서 컴파일러는 추론된 반환 형식이 있는 함수에 대해 mangled 이름을 생성하는 방법을 변경했습니다. 예를 들어 다음 함수를 살펴보세요.

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

이전 버전의 컴파일러는 링커에 대해 다음과 같은 이름을 생성했습니다.

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

함수 본문의 로컬 람다로 인해 발생하는 다른 의미 체계 동작 때문에 g에서 반환 형식이 생략될 수 있습니다. 이러한 불일치로 인해 추론된 반환 형식이 있는 내보낸 함수를 구현하기 어려웠습니다. 모듈 인터페이스에는 함수 본문이 컴파일되는 방법에 대한 정보가 필요합니다. 정의에 올바르게 연결될 수 있는 가져오기 쪽에서 함수를 생성하는 데 이 정보가 필요합니다.

이제 컴파일러에서 추론된 반환 형식 함수의 반환 형식을 생략합니다. 이 동작은 다른 주요 구현과 일치합니다. 함수 템플릿에 대한 예외가 있습니다. 즉, 이 버전의 컴파일러에서는 추론된 반환 형식이 있는 함수 템플릿에 대해 새로운 mangled 이름 동작을 도입했습니다.

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)에 대해 추론된 반환 형식이 아닌 mangled 이름이 이진에 표시됩니다.

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)

이전 버전의 컴파일러에서는 추론된 반환 형식을 시그니처의 일부로 포함했습니다. 컴파일러가 반환 형식을 mangled 이름에 포함하면 링커 문제가 발생할 수 있습니다. 잘 구성된 일부 시나리오가 링커에 모호해질 수 있습니다.

새 컴파일러 동작은 이진 주요 변경 내용을 생성할 수 있습니다. 다음 예제를 고려해 보세요.

원본 파일 a.cpp:

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

원본 파일 main.cpp:

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

버전 16.10 이전 버전에서는 의미 체계가 다른 함수이더라도 컴파일러가 int f()처럼 보이는 auto 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>);

개발자에게 <vector> 모듈에 m의 콘텐츠를 포함하려는 의도가 없었을 수 있습니다. 이제 컴파일러는 문제를 찾아 해결하는 데 도움을 주기 위해 경고를 발생합니다.

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 함수 및 익명 네임스페이스의 참조되지 않은 함수에 대해서도 경고를 발생합니다. 다음 함수에 대해 off-by-default 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 이상에서 컴파일러는 하위 개체를 중괄호로 묶지 않은 초기화 목록에 대해 경고를 표시합니다. 컴파일러는 off-by-default 경고 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++ 언어 규칙