Bagikan melalui


Gambaran umum prapemrosan baru MSVC

Visual Studio 2015 menggunakan preprocessor tradisional, yang tidak sesuai dengan Standard C++ atau C99. Mulai Visual Studio 2019 versi 16.5, dukungan prapemrosedur baru untuk standar C++20 selesai untuk fitur. Perubahan ini tersedia dengan menggunakan sakelar kompilator /Zc:preprocessor . Versi eksperimental dari preprocessor baru tersedia mulai visual Studio 2017 versi 15.8 dan yang lebih baru dengan menggunakan sakelar kompilator /experimental:preprocessor . Informasi selengkapnya tentang menggunakan prapemroscessor baru di Visual Studio 2017 dan Visual Studio 2019 tersedia. Untuk melihat dokumentasi untuk versi Visual Studio pilihan Anda, gunakan kontrol pemilih Versi. Kontrol tersebut dapat ditemukan di bagian atas daftar isi pada halaman ini.

Kami memperbarui prapemrosedaan Microsoft C++ untuk meningkatkan kesamaan standar, memperbaiki bug yang sudah lama, dan mengubah beberapa perilaku yang secara resmi tidak terdefinisi. Kami juga telah menambahkan diagnostik baru untuk memperingatkan kesalahan dalam definisi makro.

Mulai Visual Studio 2019 versi 16.5, dukungan prapemrosedaan untuk standar C++20 selesai untuk fitur. Perubahan ini tersedia dengan menggunakan sakelar kompilator /Zc:preprocessor . Versi eksperimental prapemroscessor baru tersedia di versi sebelumnya mulai dari Visual Studio 2017 versi 15.8. Anda dapat mengaktifkannya dengan menggunakan sakelar kompilator /experimental:preprocessor . Perilaku prapemrosisi default tetap sama seperti pada versi sebelumnya.

Makro baru yang telah ditentukan sebelumnya

Anda dapat mendeteksi preprosektor mana yang digunakan pada waktu kompilasi. Periksa nilai makro _MSVC_TRADITIONAL yang telah ditentukan sebelumnya untuk mengetahui apakah praprosesor tradisional sedang digunakan. Makro ini diatur tanpa syarat oleh versi kompilator yang mendukungnya, terlepas dari praprosesor mana yang dipanggil. Nilainya adalah 1 untuk prapemrosana tradisional. Ini 0 untuk prapemrosea yang sesuai.

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

Perubahan perilaku dalam prapemrosan baru

Pekerjaan awal pada prapemroses baru telah difokuskan untuk membuat semua ekspansi makro sesuai dengan standar. Ini memungkinkan Anda menggunakan pengkompilasi MSVC dengan pustaka yang saat ini diblokir oleh perilaku tradisional. Kami menguji prapemroseduran yang diperbarui pada proyek dunia nyata. Berikut adalah beberapa perubahan melanggar yang lebih umum yang kami temukan:

Komentar makro

Praprosesor tradisional didasarkan pada buffer karakter daripada token praprosesor. Ini memungkinkan perilaku yang tidak biasa seperti trik komentar prapemroses berikut, yang tidak berfungsi di bawah prapemroses yang sesuai:

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

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

Perbaikan yang sesuai standar adalah mendeklarasikan int myVal di dalam arahan yang sesuai #ifdef/#endif :

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

Praprosesor tradisional salah menggabungkan awalan string ke hasil operator stringizing (#):

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

const wchar_t *info = DEBUG_INFO(hello world);

Dalam hal ini, L awalan tidak perlu karena literal string yang berdekatan digabungkan setelah ekspansi makro pula. Perbaikan yang kompatibel dengan mundur adalah mengubah definisi:

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

Masalah yang sama juga ditemukan dalam makro kenyamanan yang "meringkas" argumen ke string literal yang luas:

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

Anda dapat memperbaiki masalah dengan berbagai cara:

  • Gunakan perangkaian string dari dan #str untuk menambahkan awalanL"". Literal string yang berdekatan digabungkan setelah ekspansi makro:

    #define STRING1(str) L""#str
    
  • Tambahkan awalan setelah #str dibentuk dengan ekspansi makro tambahan

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Gunakan operator ## penggabungan untuk menggabungkan token. Urutan operasi untuk ## dan # tidak ditentukan, meskipun semua kompilator tampaknya mengevaluasi # operator sebelumnya ## dalam kasus ini.

    #define STRING3(str) L## #str
    

Peringatan pada tidak valid ##

Ketika operator penempelan token (##) tidak menghasilkan satu token pra-pemrosesan yang valid, perilaku tidak terdefinisi. Pra-proses tradisional secara diam-diam gagal menggabungkan token. Praprosesor baru cocok dengan perilaku sebagian besar pengkompilasi lain dan memancarkan diagnostik.

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

Elisi koma dalam makro variadik

Prapemrosisi MSVC tradisional selalu menghapus koma sebelum penggantian kosong __VA_ARGS__ . Preprocessor baru lebih dekat mengikuti perilaku kompilator lintas platform populer lainnya. Agar koma dihapus, argumen variadik harus hilang (tidak hanya kosong) dan harus ditandai dengan ## operator. Pertimbangkan contoh berikut:

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

Dalam contoh berikut, dalam panggilan ke FUNC2(1) argumen variadik hilang dalam makro yang dipanggil. Dalam panggilan ke FUNC2(1, ) argumen variadik kosong, tetapi tidak hilang (perhatikan koma dalam daftar argumen).

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

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

Dalam standar C++20 yang akan datang, masalah ini telah diatasi dengan menambahkan __VA_OPT__. Dukungan praproscesor baru untuk __VA_OPT__ tersedia mulai dari Visual Studio 2019 versi 16.5.

Ekstensi makro variadik C++20

Praprosesor baru mendukung elisi argumen makro variadik C++20:

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

Kode ini tidak sesuai sebelum standar C++20. Di MSVC, prapemrosedur baru memperluas perilaku C++20 ini ke mode standar bahasa yang lebih rendah (/std:c++14, /std:c++17). Ekstensi ini cocok dengan perilaku pengkompilasi C++ lintas platform utama lainnya.

Argumen makro "dibongkar"

Di praprosesor tradisional, jika makro meneruskan salah satu argumennya ke makro dependen lain, maka argumen tidak akan "dibongkar" saat disisipkan. Biasanya pengoptimalan ini tidak diperhatikan, tetapi dapat menyebabkan perilaku yang tidak biasa:

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

Saat memperluas A(), preprosedur tradisional meneruskan semua argumen yang dikemas __VA_ARGS__ ke argumen pertama TWO_STRINGS, yang membuat argumen TWO_STRINGS variadik kosong. Itu menyebabkan hasilnya #first adalah "1, 2" daripada hanya "1". Jika Anda mengikuti dengan dekat, maka Anda mungkin bertanya-tanya apa yang terjadi pada hasil dari #__VA_ARGS__ ekspansi praprosesor tradisional: jika parameter variadik kosong, itu harus menghasilkan string kosong literal "". Masalah terpisah menjaga token literal string kosong tidak dihasilkan.

Memindai ulang daftar penggantian untuk makro

Setelah makro diganti, token yang dihasilkan dipindai kembali agar pengidentifikasi makro tambahan diganti. Algoritma yang digunakan oleh pra-proses tradisional untuk melakukan pemulaian ulang tidak sesuai, seperti yang ditunjukkan dalam contoh ini berdasarkan kode aktual:

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

Meskipun contoh ini mungkin tampak sedikit sulit, kita telah melihatnya dalam kode dunia nyata.

Untuk melihat apa yang terjadi, kita dapat memecah ekspansi dimulai dengan DO_THING:

  1. DO_THING(1, "World") meluas ke CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) memperluas ke IMPL ## 1, yang diperluas ke IMPL1
  3. Sekarang token dalam status ini: IMPL1 ECHO(("Hello", "World"))
  4. Prapemroses menemukan pengidentifikasi IMPL1makro seperti fungsi . Karena tidak diikuti oleh (, ini tidak dianggap sebagai pemanggilan makro seperti fungsi.
  5. Prapemroses berpindah ke token berikut. Ini menemukan makro ECHO seperti fungsi dipanggil: ECHO(("Hello", "World")), yang diperluas ke ("Hello", "World")
  6. IMPL1 tidak pernah dipertimbangkan lagi untuk ekspansi, sehingga hasil lengkap dari ekspansi adalah: IMPL1("Hello", "World");

Untuk memodifikasi makro agar berulah dengan cara yang sama di bawah praprosesor baru dan praprosesor tradisional, tambahkan lapisan tidak langsung lainnya:

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

Fitur yang tidak lengkap sebelum 16.5

Mulai dari Visual Studio 2019 versi 16.5, prapemroscesor baru lengkap untuk C++20. Di versi Visual Studio sebelumnya, praproscesor baru sebagian besar lengkap, meskipun beberapa logika direktif praproscesor masih kembali ke perilaku tradisional. Berikut adalah daftar sebagian fitur yang tidak lengkap di versi Visual Studio sebelum 16.5:

  • Dukungan untuk _Pragma
  • Fitur C++20
  • Meningkatkan bug pemblokiran: Operator logis dalam ekspresi konstanta prapemroseduran tidak sepenuhnya diimplementasikan dalam prapemroseduran baru sebelum versi 16.5. Pada beberapa #if arahan, prapemrosana baru dapat kembali ke prapemrossor tradisional. Efeknya hanya terlihat ketika makro tidak kompatibel dengan praprosesor tradisional diperluas. Hal ini dapat terjadi saat membangun slot praproscesor Boost.