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
시작하는 확장을 세분화할 수 있습니다.
DO_THING(1, "World")
으로 확장CAT(IMPL, 1) ECHO(("Hello", "World"))
CAT(IMPL, 1)
으로IMPL ## 1
확장됩니다.IMPL1
- 이제 토큰이 이 상태에 있습니다.
IMPL1 ECHO(("Hello", "World"))
- 전처리기는 함수와 유사한 매크로 식별자를 찾습니다
IMPL1
. 뒤에(
는 함수가 없으므로 함수와 유사한 매크로 호출로 간주되지 않습니다. - 전처리기는 다음 토큰으로 이동합니다. 함수와 유사한 매크로
ECHO
가 호출되는ECHO(("Hello", "World"))
것을 찾습니다.("Hello", "World")
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 전처리기 슬롯을 빌드할 때 발생할 수 있습니다.