MSVC 새 전처리기 개요

Visual Studio 2015는 표준 C++ 또는 C99를 준수하지 않는 기존 전처리기를 사용합니다. Visual Studio 2019 버전 16.5부터 C++20 표준에 대한 새로운 전처리기 지원이 기능 완성되었습니다. 이러한 변경 내용은 /Zc:전처리기 컴파일러 스위치를 사용하여 사용할 수 있습니다. 새 전처리기의 실험적 버전은 Visual Studio 2017 버전 15.8 이상부터 /experimental:preprocessor 컴파일러 스위치를 사용하여 사용할 수 있습니다. Visual Studio 2017 및 Visual Studio 2019에서 새 전처리기를 사용하는 방법에 대한 자세한 내용을 확인할 수 있습니다. 기본 설정된 버전의 Visual Studio에 대한 설명서를 보려면 버전 선택기 컨트롤을 사용하세요. 이 페이지의 목차 맨 위에 있습니다.

Microsoft C++ 전처리기를 업데이트하여 표준 준수를 개선하고, 오랜 버그를 수정하고, 공식적으로 정의되지 않은 일부 동작을 변경합니다. 매크로 정의의 오류에 대해 경고하는 새로운 진단 추가했습니다.

Visual Studio 2019 버전 16.5부터 C++20 표준에 대한 전처리기 지원이 기능 완성됩니다. 이러한 변경 내용은 /Zc:전처리기 컴파일러 스위치를 사용하여 사용할 수 있습니다. 새 전처리기의 실험적 버전은 Visual Studio 2017 버전 15.8부터 이전 버전에서 사용할 수 있습니다. /experimental:preprocessor 컴파일러 스위치를 사용하여 사용하도록 설정할 수 있습니다. 기본 전처리기 동작은 이전 버전과 동일하게 다시 기본.

미리 정의된 새 매크로

컴파일 시간에 사용 중인 전처리기를 검색할 수 있습니다. 미리 정의된 매크로 _MSVC_TRADITIONAL 의 값을 확인하여 기존 전처리기가 사용 중인지 확인합니다. 이 매크로는 전처리기가 호출되는 것과 관계없이 매크로를 지원하는 컴파일러 버전에 의해 무조건 설정됩니다. 기존 전처리기 값은 1입니다. 준수 전처리기용은 0입니다.

#if !defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL
// Logic using the traditional preprocessor
#else
// Logic using cross-platform compatible preprocessor
#endif

새 전처리기에서 동작 변경

새 전처리기 초기 작업은 모든 매크로 확장이 표준을 준수하도록 하는 데 중점을 두어 왔습니다. MSVC 컴파일러를 기존 동작에 의해 현재 차단된 라이브러리와 함께 사용할 수 있습니다. 실제 프로젝트에서 업데이트된 전처리기를 테스트했습니다. 다음은 우리가 찾은 몇 가지 일반적인 호환성이 손상되는 변경 내용입니다.

매크로 메모

기존 전처리기는 전처리기 토큰이 아닌 문자 버퍼를 기반으로 합니다. 다음 전처리기 주석 트릭과 같은 비정상적인 동작이 허용되며, 이는 준수 전처리기에서 작동하지 않습니다.

#if DISAPPEAR
#define DISAPPEARING_TYPE /##/
#else
#define DISAPPEARING_TYPE int
#endif

// myVal disappears when DISAPPEARING_TYPE is turned into a comment
DISAPPEARING_TYPE myVal;

표준 준수 수정은 적절한 #ifdef/#endif 지시문 내에서 선언 int myVal 하는 것입니다.

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

기존 전처리기는 문자열 접두사를 문자열화 연산자(#) 연산자의 결과에 잘못 결합합니다.

#define DEBUG_INFO(val) L"debug prefix:" L#val
//                                       ^
//                                       this prefix

const wchar_t *info = DEBUG_INFO(hello world);

이 경우 L 매크로 확장 후에 인접한 문자열 리터럴이 결합되기 때문에 접두사는 필요하지 않습니다. 이전 버전과 호환되는 수정 사항은 정의를 변경하는 것입니다.

#define DEBUG_INFO(val) L"debug prefix:" #val
//                                       ^
//                                       no prefix

인수를 와이드 문자열 리터럴로 "문자열화"하는 편리한 매크로에서도 동일한 문제가 있습니다.

 // The traditional preprocessor creates a single wide string literal token
#define STRING(str) L#str

다음과 같은 다양한 방법으로 문제를 해결할 수 있습니다.

  • 문자열 연결을 L""#str 사용하고 접두사를 추가합니다. 인접 문자열 리터럴은 매크로 확장 후 결합됩니다.

    #define STRING1(str) L""#str
    
  • 추가 매크로 확장으로 문자열화된 후 #str 접두사 추가

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • 연결 연산 ## 자를 사용하여 토큰을 결합합니다. 모든 컴파일러가 이 경우 이전에 ## 연산자를 평가하는 # 것처럼 보이지만 지정되지 않은 작업의 ### 순서입니다.

    #define STRING3(str) L## #str
    

잘못된 경고 ##

토큰 붙여넣기 연산자(##)가 유효한 단일 전처리 토큰을 생성하지 않으면 동작이 정의되지 않습니다. 기존 전처리기는 토큰을 자동으로 결합하지 못합니다. 새 전처리기는 대부분의 다른 컴파일러의 동작과 일치하고 진단을 내보낸다.

// The ## is unnecessary and does not result in a single preprocessing token.
#define ADD_STD(x) std::##x
// Declare a std::string
ADD_STD(string) s;

variadic 매크로의 쉼표 엘리션

기존 MSVC 전처리기는 항상 빈 __VA_ARGS__ 교체 전에 쉼표가 제거됩니다. 새 전처리기는 다른 인기 있는 플랫폼 간 컴파일러의 동작을 더 밀접하게 따릅니다. 쉼표가 제거되려면 variadic 인수가 누락되어야 하며(비어 있는 것이 아니라) 연산자를 ## 사용하여 표시해야 합니다. 다음 예제를 참조하세요.

void func(int, int = 2, int = 3);
// This macro replacement list has a comma followed by __VA_ARGS__
#define FUNC(a, ...) func(a, __VA_ARGS__)
int main()
{
    // In the traditional preprocessor, the
    // following macro is replaced with:
    // func(10,20,30)
    FUNC(10, 20, 30);

    // A conforming preprocessor replaces the
    // following macro with: func(1, ), which
    // results in a syntax error.
    FUNC(1, );
}

다음 예제에서는 variadic 인수에 대한 FUNC2(1) 호출에서 호출되는 매크로에 누락되었습니다. variadic 인수 호출 FUNC2(1, ) 에서 비어 있지만 누락되지 않았습니다(인수 목록의 쉼표에 유의).

#define FUNC2(a, ...) func(a , ## __VA_ARGS__)
int main()
{
   // Expands to func(1)
   FUNC2(1);

   // Expands to func(1, )
   FUNC2(1, );
}

예정된 C++20 표준에서는 이 문제를 추가하여 __VA_OPT__해결되었습니다. Visual Studio 2019 버전 16.5부터 새로운 전처리기 지원을 __VA_OPT__ 사용할 수 있습니다.

C++20 variadic 매크로 확장

새 전처리기는 C++20 variadic 매크로 인수 엘리션을 지원합니다.

#define FUNC(a, ...) __VA_ARGS__ + a
int main()
  {
  int ret = FUNC(0);
  return ret;
  }

이 코드는 C++20 표준 이전에는 호환되지 않습니다. MSVC에서 새 전처리기는 이 C++20 동작을 낮은 언어 표준 모드(/std:c++14, /std:c++17)로 확장합니다. 이 확장은 다른 주요 플랫폼 간 C++ 컴파일러의 동작과 일치합니다.

매크로 인수는 "압축 풀기"입니다.

기존 전처리기에서 매크로가 인수 중 하나를 다른 종속 매크로로 전달하면 인수가 삽입될 때 "압축 해제"되지 않습니다. 일반적으로 이 최적화는 눈에 띄지 않지만 비정상적인 동작으로 이어질 수 있습니다.

// Create a string out of the first argument, and the rest of the arguments.
#define TWO_STRINGS( first, ... ) #first, #__VA_ARGS__
#define A( ... ) TWO_STRINGS(__VA_ARGS__)
const char* c[2] = { A(1, 2) };

// Conforming preprocessor results:
// const char c[2] = { "1", "2" };

// Traditional preprocessor results, all arguments are in the first string:
// const char c[2] = { "1, 2", };

확장할 A()때 기존 전처리기는 패키지된 __VA_ARGS__ 모든 인수를 TWO_STRINGS 첫 번째 인수로 전달하여 variadic 인수를 TWO_STRINGS 비워 둡니다. 그러면 결과가 #first "1"이 아니라 "1, 2"가 됩니다. 자세히 따라가는 경우 기존 전처리기 확장의 #__VA_ARGS__ 결과로 어떤 일이 발생했는지 궁금할 수 있습니다. variadic 매개 변수가 비어 있으면 빈 문자열 리터럴 ""이 발생합니다. 별도의 문제로 인해 빈 문자열 리터럴 토큰이 생성되지 않습니다.

매크로에 대한 대체 목록 다시 검색

매크로가 교체되면 대체될 추가 매크로 식별자를 위해 결과 토큰이 다시 검사됩니다. 실제 코드를 기반으로 하는 이 예제와 같이 기존 전처리기에서 다시 검사하는 데 사용하는 알고리즘이 준수하지 않습니다.

#define CAT(a,b) a ## b
#define ECHO(...) __VA_ARGS__
// IMPL1 and IMPL2 are implementation details
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)

// MACRO chooses the expansion behavior based on the value passed to macro_switch
#define DO_THING(macro_switch, b) CAT(IMPL, macro_switch) ECHO(( "Hello", b))
DO_THING(1, "World");

// Traditional preprocessor:
// do_thing_one( "Hello", "World");
// Conforming preprocessor:
// IMPL1 ( "Hello","World");

이 예제는 약간 모순된 것처럼 보일 수 있지만 실제 코드에서 살펴보았습니다.

진행 중인 작업을 확인하기 위해 다음으로 DO_THING시작하는 확장을 세분화할 수 있습니다.

  1. DO_THING(1, "World") 으로 확장 CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) 으로 IMPL ## 1확장됩니다. IMPL1
  3. 이제 토큰이 이 상태에 있습니다. IMPL1 ECHO(("Hello", "World"))
  4. 전처리기는 함수와 유사한 매크로 식별자를 찾습니다 IMPL1. 뒤에 (는 함수가 없으므로 함수와 유사한 매크로 호출로 간주되지 않습니다.
  5. 전처리기는 다음 토큰으로 이동합니다. 함수와 유사한 매크로 ECHO 가 호출되는 ECHO(("Hello", "World"))것을 찾습니다. ("Hello", "World")
  6. IMPL1 는 확장에 대해 다시는 고려되지 않으므로 확장의 전체 결과는 다음과 같습니다. IMPL1("Hello", "World");

새 전처리기와 기존 전처리기 모두에서 동일한 방식으로 동작하도록 매크로를 수정하려면 다른 간접 참조 계층을 추가합니다.

#define CAT(a,b) a##b
#define ECHO(...) __VA_ARGS__
// IMPL1 and IMPL2 are macros implementation details
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)
#define CALL(macroName, args) macroName args
#define DO_THING_FIXED(a,b) CALL( CAT(IMPL, a), ECHO(( "Hello",b)))
DO_THING_FIXED(1, "World");

// macro expands to:
// do_thing_one( "Hello", "World");

16.5 이전의 불완전한 기능

Visual Studio 2019 버전 16.5부터 새로운 전처리기는 C++20에 대한 기능 완성입니다. 이전 버전의 Visual Studio에서는 새 전처리기가 대부분 완료되었지만 일부 전처리기 지시문 논리는 여전히 기존 동작으로 돌아갑니다. 다음은 16.5 이전의 Visual Studio 버전에서 불완전한 기능의 일부 목록입니다.

  • _Pragma 지원
  • C++20 기능
  • 차단 버그 향상: 전처리기 상수 식의 논리 연산자는 버전 16.5 이전의 새 전처리기에서 완전히 구현되지 않습니다. 일부 #if 지시문에서 새 전처리기는 기존 전처리기로 대체됩니다. 이 효과는 기존 전처리기와 호환되지 않는 매크로가 확장될 때만 눈에 띄습니다. Boost 전처리기 슬롯을 빌드할 때 발생할 수 있습니다.