Udostępnij za pośrednictwem


Omówienie nowego preprocesora MSVC

Program Visual Studio 2015 używa tradycyjnego preprocesora, który nie jest zgodny z standardem C++ lub C99. Począwszy od programu Visual Studio 2019 w wersji 16.5, nowa obsługa preprocesora dla standardu C++20 jest kompletna. Te zmiany są dostępne za pomocą przełącznika kompilatora /Zc:preprocesor . Eksperymentalna wersja nowego preprocesora jest dostępna od programu Visual Studio 2017 w wersji 15.8 lub nowszej przy użyciu przełącznika kompilatora /experimental:preprocessor . Więcej informacji na temat korzystania z nowego preprocesora w programach Visual Studio 2017 i Visual Studio 2019 jest dostępnych. Aby zapoznać się z dokumentacją preferowanej wersji programu Visual Studio, użyj kontrolki selektora wersji . Znajduje się on w górnej części spisu treści na tej stronie.

Aktualizujemy preprocesor języka Microsoft C++, aby poprawić zgodność ze standardami, naprawić długotrwałe usterki i zmienić niektóre zachowania, które są oficjalnie niezdefiniowane. Dodaliśmy również nową diagnostykę ostrzegawczą o błędach w definicjach makr.

Począwszy od programu Visual Studio 2019 w wersji 16.5, obsługa preprocesora dla standardu C++20 jest kompletna. Te zmiany są dostępne za pomocą przełącznika kompilatora /Zc:preprocesor . Eksperymentalna wersja nowego preprocesora jest dostępna we wcześniejszych wersjach, począwszy od programu Visual Studio 2017 w wersji 15.8. Można ją włączyć za pomocą przełącznika kompilatora /experimental:preprocesor . Domyślne zachowanie preprocesora pozostaje takie samo jak w poprzednich wersjach.

Nowe wstępnie zdefiniowane makro

Możesz wykryć, który preprocesor jest używany w czasie kompilacji. Sprawdź wartość wstępnie zdefiniowanego makra _MSVC_TRADITIONAL , aby określić, czy jest używany tradycyjny preprocesor. To makro jest ustawiane bezwarunkowo przez wersje kompilatora, które go obsługują, niezależnie od tego, który preprocesor jest wywoływany. Jego wartość to 1 dla tradycyjnego preprocesora. Jest to wartość 0 dla zgodnego preprocesora.

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

Zmiany zachowania w nowym preprocesorze

Wstępne prace nad nowym preprocesorem koncentrowały się na tym, aby wszystkie rozszerzenia makr były zgodne ze standardem. Umożliwia korzystanie z kompilatora MSVC z bibliotekami, które są obecnie blokowane przez tradycyjne zachowania. Przetestowaliśmy zaktualizowany preprocesor w rzeczywistych projektach. Oto niektóre z bardziej typowych zmian powodujących niezgodność:

Komentarze makr

Tradycyjny preprocesor jest oparty na znaków, a nie na tokenach preprocesora. Umożliwia to nietypowe zachowanie, takie jak następująca sztuczka komentarza preprocesora, która nie działa w ramach zgodnego preprocesora:

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

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

Poprawka zgodna ze standardami jest deklarowana int myVal wewnątrz odpowiednich #ifdef/#endif dyrektyw:

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

Tradycyjny preprocesor niepoprawnie łączy prefiks ciągu z wynikiem operatora stringizing (#):

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

const wchar_t *info = DEBUG_INFO(hello world);

W takim przypadku prefiks jest niepotrzebny, L ponieważ sąsiadujące literały ciągów są łączone po rozszerzeniu makra. Poprawka zgodna z poprzednimi wersjami polega na zmianie definicji:

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

Ten sam problem występuje również w makrach wygody, które "ciągują" argument do literału szerokiego ciągu:

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

Problem można rozwiązać na różne sposoby:

  • Użyj łączenia ciągów L"" i #str , aby dodać prefiks. Sąsiadujące literały ciągów są łączone po rozszerzeniu makra:

    #define STRING1(str) L""#str
    
  • Dodaj prefiks po #str parametrze z dodatkowym rozszerzeniem makra

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Użyj operatora ## łączenia, aby połączyć tokeny. Kolejność operacji i ## # nie jest określona, chociaż wszystkie kompilatory wydają się oceniać # operator przed ## w tym przypadku.

    #define STRING3(str) L## #str
    

Ostrzeżenie dotyczące nieprawidłowego ##

Gdy operator wklejania tokenów (##) nie powoduje utworzenia pojedynczego prawidłowego tokenu przetwarzania wstępnego, zachowanie jest niezdefiniowane. Tradycyjny preprocesor dyskretnie nie może połączyć tokenów. Nowy preprocesor odpowiada zachowaniu większości innych kompilatorów i emituje diagnostykę.

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

Elizja przecinka w makrach wariadycznych

Tradycyjny preprocesor MSVC zawsze usuwa przecinki przed pustymi __VA_ARGS__ zamianami. Nowy preprocesor dokładniej śledzi zachowanie innych popularnych kompilatorów międzyplatformowych. Aby przecinek do usunięcia, argument wariadyczny musi brakować (nie tylko puste) i musi być oznaczony operatorem ## . Rozważmy następujący przykład:

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

W poniższym przykładzie w wywołaniu FUNC2(1) argumentu wariadycznego brakuje w wywoływanym makrze. W wywołaniu FUNC2(1, ) argumentu variadic jest pusty, ale nie brakuje (zwróć uwagę na przecinek na liście argumentów).

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

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

W nadchodzącym standardzie C++20 ten problem został rozwiązany przez dodanie __VA_OPT__elementu . Dostępna jest nowa obsługa __VA_OPT__ preprocesora dla programu Visual Studio 2019 w wersji 16.5.

Rozszerzenie makra Variadic języka C++20

Nowy preprocesor obsługuje elision argumentu makra Variadic języka C++20:

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

Ten kod nie jest zgodny przed standardem C++20. W środowisku MSVC nowy preprocesor rozszerza to zachowanie języka C++20 na niższe tryby standardowe języka (/std:c++14, /std:c++17). To rozszerzenie jest zgodne z zachowaniem innych głównych kompilatorów języka C++ dla wielu platform.

Argumenty makr są "rozpakowane"

W tradycyjnym preprocesorze, jeśli makro przekazuje jeden z jego argumentów do innego makra zależnego, argument nie otrzymuje "rozpakowanego" po wstawieniu. Zazwyczaj ta optymalizacja jest niezauważona, ale może prowadzić do nietypowego zachowania:

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

Podczas rozszerzania A()metody tradycyjny preprocesor przekazuje wszystkie argumenty spakowane __VA_ARGS__ do pierwszego argumentu TWO_STRINGS, który pozostawia wariadyczny argument pustego TWO_STRINGS . Powoduje to, że wynik #first to "1, 2", a nie tylko "1". Jeśli uważnie obserwujesz, być może zastanawiasz się, co się stało z #__VA_ARGS__ wynikiem tradycyjnego rozszerzenia preprocesora: jeśli parametr variadyczny jest pusty, powinien spowodować pusty literał ""ciągu . Oddzielny problem uniemożliwia generowanie pustego tokenu literału ciągu.

Ponowne skanowanie listy zastępczej dla makr

Po zastąpieniu makra wynikowe tokeny są ponownie skanowane, aby zastąpić dodatkowe identyfikatory makr. Algorytm używany przez tradycyjny preprocesor do ponownego skanowania nie jest zgodny, jak pokazano w tym przykładzie na podstawie rzeczywistego kodu:

#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");

Chociaż ten przykład może wydawać się nieco zawstydzony, widzieliśmy go w kodzie rzeczywistym.

Aby zobaczyć, co się dzieje, możemy podzielić rozszerzenie, zaczynając od DO_THING:

  1. DO_THING(1, "World") rozwija się do CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) rozwija się do IMPL ## 1elementu , co powoduje rozszerzenie do IMPL1
  3. Teraz tokeny są w tym stanie: IMPL1 ECHO(("Hello", "World"))
  4. Preprocesor znajduje identyfikator IMPL1makra przypominający funkcję . Ponieważ nie (następuje po nim element , nie jest uważany za wywołanie makra przypominające funkcję.
  5. Preprocesor przechodzi do następujących tokenów. Znajduje ono wywoływane makro ECHO podobne do funkcji: ECHO(("Hello", "World")), które rozszerza się na ("Hello", "World")
  6. IMPL1 nigdy nie jest brany pod uwagę w celu rozszerzenia, więc pełny wynik rozszerzeń jest: IMPL1("Hello", "World");

Aby zmodyfikować makro tak samo jak w przypadku nowego preprocesora i tradycyjnego preprocesora, dodaj kolejną warstwę pośrednią:

#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");

Niekompletne funkcje przed 16.5

Począwszy od programu Visual Studio 2019 w wersji 16.5, nowy preprocesor jest kompletny dla języka C++20. W poprzednich wersjach programu Visual Studio nowy preprocesor jest w większości kompletny, chociaż niektóre logiki dyrektywy preprocesora nadal wracają do tradycyjnego zachowania. Oto częściowa lista niekompletnych funkcji w programie Visual Studio w wersjach wcześniejszych niż 16.5:

  • Obsługa _Pragma
  • Funkcje języka C++20
  • Zwiększenie blokowania usterki: Operatory logiczne w wyrażeniach stałych preprocesora nie są w pełni implementowane w nowym preprocesorze przed wersją 16.5. W niektórych #if dyrektywach nowy preprocesor może wrócić do tradycyjnego preprocesora. Efekt jest zauważalny tylko wtedy, gdy makra niezgodne z tradycyjnym preprocesorem są rozszerzane. Może się to zdarzyć podczas kompilowania gniazd preprocesora Boost.