C ++: sesi arkeologi spontan dan mengapa Anda tidak harus menggunakan fungsi variabel dalam gaya C

Semuanya dimulai, seperti biasa, dengan kesalahan. Ini adalah pertama kalinya saya bekerja dengan Java Native Interface dan di bagian C ++ saya membungkus fungsi yang membuat objek Java. Fungsi ini - CallVoidMethod - adalah variabel, mis. selain penunjuk ke lingkungan JNI , penunjuk ke jenis objek yang akan dibuat, dan pengidentifikasi untuk metode yang disebut (dalam hal ini, konstruktor), diperlukan sejumlah argumen lain yang sewenang-wenang. Itu logis, karena argumen lain ini diteruskan ke metode yang disebut di sisi Jawa, dan metode dapat berbeda, dengan jumlah argumen yang berbeda dari jenis apa pun.

Oleh karena itu, saya juga membuat variabel pembungkus saya. Untuk meneruskan sejumlah argumen sewenang-wenang ke CallVoidMethod menggunakan va_list , karena berbeda dalam hal ini. Ya, itulah yang dikirim CallVoidMethod ke CallVoidMethod . Dan menjatuhkan kesalahan segmentasi dangkal JVM.

Dalam 2 jam saya berhasil mencoba beberapa versi JVM, dari tanggal 8 hingga 11, karena: pertama, ini adalah pengalaman pertama saya dengan JVM , dan dalam hal ini saya lebih mempercayai StackOverflow daripada saya, dan kedua, seseorang kemudian pada StackOverflow saya menyarankan dalam hal ini untuk menggunakan bukan OpenJDK, tetapi OracleJDK, dan bukan 8, tapi 10. Dan hanya kemudian saya akhirnya menyadari bahwa selain variabel CallVoidMethod ada CallVoidMethodV , yang mengambil sejumlah argumen melalui va_list .

Yang paling tidak saya sukai dari cerita ini adalah saya tidak segera melihat perbedaan antara ellipsis (ellipsis) dan va_list . Dan setelah memperhatikan, saya tidak dapat menjelaskan kepada diri saya apa perbedaan mendasar itu. Jadi, kita perlu berurusan dengan ellipsis, dan dengan va_list , dan (karena kita masih berbicara tentang C ++) dengan templat variabel.

Bagaimana dengan elipsis dan va_list dikatakan dalam Standar


Standar C ++ hanya menjelaskan perbedaan antara persyaratannya dan persyaratan Standar C. Perbedaan itu sendiri akan dibahas nanti, tetapi untuk saat ini saya akan menjelaskan secara singkat apa yang dikatakan Standar C (dimulai dengan C89).

  • Anda dapat mendeklarasikan fungsi yang mengambil sejumlah argumen sembarang. Yaitu suatu fungsi dapat memiliki lebih banyak argumen daripada parameter. Untuk melakukan ini, daftar parameternya harus diakhiri dengan elipsis, tetapi setidaknya satu parameter tetap [C11 6.9.1 / 8] juga harus ada:

     void foo(int parm1, int parm2, ...); 
  • Informasi tentang jumlah dan jenis argumen yang sesuai dengan elipsis tidak diteruskan ke fungsi itu sendiri. Yaitu setelah parameter bernama terakhir ( parm2 pada contoh di atas) [C11 6.7.6.3/9] .
  • Untuk mengakses argumen ini, Anda harus menggunakan tipe va_list dideklarasikan di header <stdarg.h> dan 4 (3 sebelum standar C11) makro: va_start , va_arg , va_end dan va_copy (dimulai dengan C11) [C11 7.16] .

    Sebagai contoh
     int add(int count, ...) { int result = 0; va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { result += va_arg(args, int); } va_end(args); return result; } 

    Ya, fungsinya tidak tahu berapa banyak argumen yang dimilikinya. Dia perlu melewati nomor ini. Dalam hal ini, melalui argumen bernama tunggal (opsi umum lainnya adalah untuk melewati NULL sebagai argumen terakhir, seperti dalam execl , atau 0).
  • Argumen yang disebutkan terakhir tidak dapat memiliki kelas penyimpanan register , tidak bisa berupa fungsi atau array. Jika tidak, perilaku tidak terdefinisi [C11 7.16.1.4/4] .
  • Selain itu, untuk argumen yang disebutkan terakhir dan untuk semua yang tidak bernama, " promosi argumen default " diterapkan ( promosi argumen default ; jika ada terjemahan yang baik dari konsep ini ke dalam bahasa Rusia, saya dengan senang hati menggunakannya). Ini berarti bahwa jika argumen memiliki tipe char , short (dengan atau tanpa tanda) atau float , maka parameter yang sesuai harus diakses sebagai int , int (dengan atau tanpa tanda) atau double . Jika tidak, perilaku tidak terdefinisi [C11 7.16.1.1/2] .
  • Tentang tipe va_list hanya dikatakan bahwa ia dideklarasikan dalam <stdarg.h> dan lengkap (yaitu, ukuran objek jenis ini diketahui) [C11 7.16 / 3] .

Mengapa Tetapi karena!


Tidak banyak jenis dalam C. Mengapa va_list dinyatakan dalam Standar, tetapi tidak ada yang dikatakan tentang struktur internalnya?

Mengapa kita membutuhkan elipsis jika sejumlah argumen sembarang fungsi dapat dilewati melalui va_list ? Bisa dikatakan sekarang: "sebagai gula sintaksis", tetapi 40 tahun yang lalu, saya yakin, tidak ada waktu untuk gula.

Philip James Plauger Phillip James Plauger dalam buku The Standard C library - 1992 - mengatakan bahwa awalnya C dibuat khusus untuk komputer PDP-11. Dan di sana dimungkinkan untuk memilah-milah semua argumen dari fungsi menggunakan aritmatika pointer sederhana. Masalahnya muncul dengan popularitas C dan transfer kompiler ke arsitektur lain. Edisi pertama Bahasa Pemrograman C oleh Brian Kernighan dan Dennis Ritchie - 1978 - secara eksplisit menyatakan:
By the way, tidak ada cara yang dapat diterima untuk menulis fungsi portabel dari sejumlah argumen, karena Tidak ada cara portabel untuk fungsi yang dipanggil untuk mengetahui berapa banyak argumen yang diteruskan ketika dipanggil. ... printf , fungsi bahasa C paling umum dari sejumlah argumen arbitrer, ... tidak portabel dan harus diterapkan untuk setiap sistem.
Buku ini menjelaskan printf , tetapi belum memiliki vprintf , dan tidak menyebutkan jenis dan makro va_* . Mereka muncul dalam edisi kedua Bahasa Pemrograman C (1988), dan ini adalah kelebihan komite untuk pengembangan Standar C pertama (C89, alias ANSI C). Komite menambahkan <stdarg.h> menuju ke Standar, mengambil sebagai dasar <varargs.h> dibuat oleh Andrew Koenig untuk meningkatkan portabilitas OS UNIX. Diputuskan untuk meninggalkan macro va_* sebagai macro sehingga akan lebih mudah bagi kompiler yang ada untuk mendukung Standar baru.

Sekarang, dengan munculnya C89 dan keluarga va_* , telah dimungkinkan untuk membuat fungsi variabel portabel. Dan meskipun struktur internal keluarga ini masih belum dijelaskan dengan cara apa pun, dan tidak ada persyaratan untuk itu, sudah jelas mengapa.

Karena penasaran, Anda dapat menemukan contoh implementasi <stdarg.h> . Misalnya, "Perpustakaan Standar C" yang sama memberikan contoh untuk Borland Turbo C ++ :

<stdarg.h> dari Borland Turbo C ++
 #ifndef _STADARG #define _STADARG #define _AUPBND 1 #define _ADNBND 1 typedef char* va_list #define va_arg(ap, T) \ (*(T*)(((ap) += _Bnd(T, _AUPBND)) - _Bnd(T, _ADNBND))) #define va_end(ap) \ (void)0 #define va_start(ap, A) \ (void)((ap) = (char*)&(A) + _Bnd(A, _AUPBND)) #define _Bnd(X, bnd) \ (sizeof(X) + (bnd) & ~(bnd)) #endif 


SystemV ABI yang jauh lebih baru untuk AMD64 menggunakan tipe ini untuk va_list :

va_list dari SystemV ABI AMD64
 typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1]; 


Secara umum, kita dapat mengatakan bahwa tipe dan makro va_* menyediakan antarmuka standar untuk melintasi argumen fungsi variabel, dan implementasinya karena alasan historis tergantung pada kompiler, platform target, dan arsitektur. Selain itu, ellipsis (mis., Fungsi variabel secara umum) muncul di C lebih awal dari va_list (mis., Header <stdarg.h> ). Dan va_list tidak dibuat untuk menggantikan elipsis, tetapi untuk memungkinkan pengembang untuk menulis fungsi variabel portabel mereka.

C ++ sebagian besar mempertahankan kompatibilitas ke belakang dengan C, jadi semua hal di atas berlaku untuk itu. Tetapi ada juga fitur.

Fungsi Variabel dalam C ++


Kelompok kerja WG21 telah terlibat dalam pengembangan Standar C ++. Pada tahun 1989, Standar C89 yang baru dibuat diambil sebagai dasar, yang secara bertahap berubah untuk menggambarkan C ++ itu sendiri. Pada tahun 1995, proposal N0695 diterima dari John Micco , di mana penulis menyarankan untuk mengubah batasan untuk macro va_* :

  • Karena C ++, tidak seperti C, memungkinkan Anda untuk mendapatkan alamat register dari variabel, maka argumen nama terakhir dari fungsi variabel dapat memiliki kelas penyimpanan ini.
  • Karena tautan yang muncul di C ++ melanggar aturan fungsi variabel C yang tidak tertulis - ukuran parameter harus cocok dengan ukuran tipe yang dideklarasikan - maka argumen yang disebutkan terakhir tidak boleh berupa tautan. Jika tidak, perilaku yang tidak jelas.
  • Karena di C ++ tidak ada konsep " meningkatkan jenis argumen secara default ", lalu frasa
    Jika parameter parmN dideklarasikan dengan ... tipe yang tidak kompatibel dengan tipe yang dihasilkan setelah aplikasi promosi argumen default, perilaku tidak terdefinisi
    harus diganti oleh
    Jika parameter parmN dideklarasikan dengan ... tipe yang tidak kompatibel dengan tipe yang dihasilkan ketika melewati argumen yang tidak ada parameter, perilaku tidak terdefinisi
Saya bahkan tidak menerjemahkan poin terakhir untuk berbagi rasa sakit saya. Pertama, " eskalasi jenis argumen default " di C ++ Standard tetap [C ++ 17 8.2.2 / 9] . Dan kedua, saya bingung untuk waktu yang lama tentang makna ungkapan ini, dibandingkan dengan Standar C, di mana semuanya jelas. Hanya setelah membaca N0695 saya akhirnya mengerti: Maksud saya hal yang sama.

Namun, ketiga perubahan diadopsi [C ++ 98 18,7 / 3] . Kembali di C ++, persyaratan untuk fungsi variabel untuk memiliki setidaknya satu parameter bernama (dalam hal ini Anda tidak dapat mengakses yang lain, tetapi lebih lanjut tentang itu nanti) telah menghilang, dan daftar tipe valid argumen tanpa nama telah ditambahkan dengan pointer ke anggota kelas dan tipe POD .

Standar C ++ 03 tidak membawa perubahan pada fungsi variasional. C ++ 11 mulai mengonversi argumen tanpa nama dari tipe std::nullptr_t menjadi void* dan mengizinkan kompiler, atas kebijakan mereka, untuk mendukung tipe dengan konstruktor dan destruktor non-sepele [C ++ 11 5.2.2 / 7] . C ++ 14 memungkinkan penggunaan fungsi dan array sebagai parameter bernama terakhir [C ++ 14 18.10 / 3] , dan C ++ 17 melarang penggunaan ekspansi paket parameter ( ekspansi paket ) dan variabel yang ditangkap oleh lambda [C ++ 17 21.10.1 / 1] .

Akibatnya, C ++ menambahkan fungsi variatif ke perangkapnya. Hanya dukungan tipe yang tidak ditentukan dengan konstruktor / destruktor non-sepele yang layak. Di bawah ini saya akan mencoba untuk mengurangi semua fitur fungsi variabel yang tidak jelas menjadi satu daftar dan menambahkannya dengan contoh-contoh spesifik.

Cara menggunakan fungsi variabel dengan mudah dan salah


  1. Tidak benar untuk mendeklarasikan argumen yang disebutkan terakhir dengan tipe yang dipromosikan, mis. char , char signed char , unsigned char , singed short , unsigned short atau float . Hasil menurut Standar akan menjadi perilaku yang tidak terdefinisi.

    Kode tidak valid
     void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 


    Dari semua kompiler yang saya miliki (gcc, dentang, MSVC), hanya dentang mengeluarkan peringatan.

    Peringatan dentang
     ./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    Dan meskipun dalam semua kasus kode yang dikompilasi berperilaku dengan benar, Anda tidak harus mengandalkannya.

    Itu akan benar
     void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  2. Tidak benar untuk menyatakan argumen yang disebutkan terakhir sebagai referensi. Tautan apa saja. Standar dalam hal ini juga menjanjikan perilaku yang tidak terdefinisi.

    Kode tidak valid
     void foo(int& n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

    gcc 7.3.0 menyusun kode ini tanpa satu komentar. lang 6.0.0 mengeluarkan peringatan, tetapi masih menyusunnya.

    Peringatan dentang
     ./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    Dalam kedua kasus, program bekerja dengan benar (beruntung, Anda tidak dapat mengandalkannya). Tetapi MSVC 19.15.26730 membedakan dirinya - ia menolak untuk mengkompilasi kode, karena Argumen va_start tidak boleh menjadi referensi.

    Kesalahan dari MSVC
     c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\vadefs.h(151): error C2338: va_start argument must not have reference type and must not be parenthesized 

    Nah, opsi yang benar terlihat, misalnya, seperti ini
     void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  3. Salah meminta va_arg menaikkan tipe - char , short atau float .

    Kode tidak valid
     #include <cstdarg> #include <iostream> void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, float) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } int main() { foo(0, 1, 2.0f, 3); return 0; } 

    Lebih menarik di sini. gcc pada saat kompilasi memberikan peringatan bahwa perlu menggunakan double daripada float , dan jika kode ini masih dijalankan, program akan berakhir dengan kesalahan.

    Peringatan gcc
     ./test.cpp:9:15: warning: 'float' is promoted to 'double' when passed through '...' std::cout << va_arg(va, float) << std::endl; ^~~~~~ ./test.cpp:9:15: note: (so you should pass 'double' not 'float' to 'va_arg') ./test.cpp:9:15: note: if this code is reached, the program will abort 

    Memang, program macet dengan keluhan tentang instruksi yang tidak valid.
    Analisis dump menunjukkan bahwa program menerima sinyal SIGILL. Dan itu juga menunjukkan struktur va_list . Untuk 32 bit ini

     va = 0xfffc6918 "" 

    yaitu va_list hanya char* . Untuk 64 bit:

     va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}} 

    yaitu persis apa yang dijelaskan dalam SystemV ABI AMD64.

    dentang pada kompilasi memperingatkan perilaku tidak terdefinisi dan juga menyarankan mengganti float dengan double .

    Peringatan dentang
     ./test.cpp:9:26: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs] std::cout << va_arg(va, float) << std::endl; ^~~~~ 

    Tetapi program tidak lagi crash, versi 32-bit menghasilkan:

     1 0 1073741824 

    64 bit:

     1 0 3 

    MSVC menghasilkan hasil yang persis sama, hanya tanpa peringatan, bahkan dengan /Wall .

    Di sini dapat diasumsikan bahwa perbedaan antara 32 dan 64 bit disebabkan oleh fakta bahwa dalam kasus pertama, ABI melewati semua argumen melalui stack ke fungsi yang disebut, dan pada yang kedua, empat (Windows) atau enam (Linux) argumen pertama melalui register prosesor, sisanya melalui susun [ wiki ]. Tetapi tidak, jika Anda memanggil foo bukan dengan 4 argumen, tetapi dengan 19, dan mengeluarkannya dengan cara yang sama, hasilnya akan sama: kekacauan penuh dalam versi 32-bit, dan nol untuk semua float di 64-bit. Yaitu intinya tentu saja di ABI, tetapi tidak dalam penggunaan register untuk mengajukan argumen.

    Ya, tentu saja, itu benar
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, double) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  4. Tidak benar untuk melewatkan instance kelas dengan konstruktor nontrivial atau destruktor sebagai argumen yang tidak disebutkan namanya. Kecuali, tentu saja, nasib kode ini menggairahkan Anda setidaknya sedikit lebih dari "kompilasi dan jalankan di sini dan sekarang."

    Kode tidak valid
     #include <cstdarg> #include <iostream> struct Bar { Bar() { std::cout << "Bar default ctor" << std::endl; } Bar(const Bar&) { std::cout << "Bar copy ctor" << std::endl; } ~Bar() { std::cout << "Bar dtor" << std::endl; } }; struct Cafe { Cafe() { std::cout << "Cafe default ctor" << std::endl; } Cafe(const Cafe&) { std::cout << "Cafe copy ctor" << std::endl; } ~Cafe() { std::cout << "Cafe dtor" << std::endl; } }; void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto b = va_arg(va, Bar); va_end(va); } int main() { Bar b; Cafe c; foo(1, b, c); return 0; } 

    Dentang lebih keras dari semua. Dia hanya menolak untuk mengkompilasi kode ini karena argumen kedua, va_arg bukan tipe POD, dan memperingatkan bahwa program akan va_arg saat startup.

    Peringatan dentang
     ./test.cpp:23:31: error: second argument to 'va_arg' is of non-POD type 'Bar' [-Wnon-pod-varargs] const auto b = va_arg(va, Bar); ^~~ ./test.cpp:31:12: error: cannot pass object of non-trivial type 'Bar' through variadic function; call will abort at runtime [-Wnon-pod-varargs] foo(1, b, c); ^ 

    Jadi, jika Anda masih mengkompilasi dengan -Wno-non-pod-varargs .

    MSVC memperingatkan bahwa penggunaan tipe dengan konstruktor non-sepele dalam hal ini tidak portabel.

    Peringatan dari MSVC
     d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840:    "Bar"          

    Tetapi kode mengkompilasi dan berjalan dengan benar. Berikut ini diperoleh di konsol:

    Peluncuran hasil
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    Yaitu salinan dibuat hanya pada saat memanggil va_arg , dan argumennya, ternyata, dilewatkan dengan referensi. Entah bagaimana itu tidak jelas, tetapi Standar mengizinkan.

    gcc 6.3.0 mengkompilasi tanpa satu komentar. Outputnya sama:

    Peluncuran hasil
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    gcc 7.3.0 juga tidak memperingatkan tentang apa pun, tetapi perilaku berubah:

    Peluncuran hasil
     Bar default ctor Cafe default ctor Cafe copy ctor Bar copy ctor Before va_arg Bar copy ctor Bar dtor Bar dtor Cafe dtor Cafe dtor Bar dtor 

    Yaitu versi kompiler ini meneruskan argumen dengan nilai, dan ketika dipanggil, va_arg membuat salinan lain. Akan menyenangkan untuk mencari perbedaan ini ketika beralih dari versi ke-6 ke gcc jika konstruktor / destruktor memiliki efek samping.

    Omong-omong, jika Anda secara eksplisit lulus dan meminta referensi ke kelas:

    Kode salah lain
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto& b = va_arg(va, Bar&); va_end(va); } int main() { Bar b; Cafe c; foo(1, std::ref(b), c); return 0; } 

    maka semua kompiler akan melakukan kesalahan. Seperti yang disyaratkan oleh Standar.

    Secara umum, jika Anda benar-benar ingin, lebih baik memberikan argumen dengan pointer.

    Seperti ini
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto* b = va_arg(va, Bar*); va_end(va); } int main() { Bar b; Cafe c; foo(1, &b, &c); return 0; } 


Resolusi Kelebihan dan Fungsi Variabel


Di satu sisi, semuanya sederhana: mencocokkan dengan elipsis lebih buruk daripada mencocokkan dengan argumen bernama biasa, bahkan dalam kasus konversi tipe standar atau yang ditentukan pengguna.

Contoh yang berlebihan
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo(int) { std::cout << "Ordinary function" << std::endl; } int main() { foo(1); foo(1ul); foo(); return 0; } 


Peluncuran hasil
 $ ./test Ordinary function Ordinary function C variadic function 

Tetapi ini hanya berfungsi sampai panggilan ke foo tanpa argumen perlu dipertimbangkan secara terpisah.

Panggil foo tanpa argumen
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } int main() { foo(1); foo(); return 0; } 

Output kompiler
 ./test.cpp:16:9: error: call of overloaded 'foo()' is ambiguous foo(); ^ ./test.cpp:3:6: note: candidate: void foo(...) void foo(...) ^~~ ./test.cpp:8:6: note: candidate: void foo() void foo() ^~~ 

Semuanya sesuai dengan Standar: tidak ada argumen - tidak ada perbandingan dengan elipsis, dan ketika kelebihan diselesaikan, fungsi variatif menjadi tidak lebih buruk dari yang biasanya.

Namun kapan layak menggunakan fungsi variabel


Nah, fungsi variatif terkadang tidak berperilaku sangat jelas dan dalam konteks C ++ dapat dengan mudah berubah menjadi portabel yang buruk. Ada banyak tips di Internet seperti "Jangan membuat atau menggunakan fungsi variabel C", tetapi mereka tidak akan menghapus dukungan mereka dari Standar C ++. Jadi ada beberapa manfaat dari fitur ini? Baik di sana.

  • Kasus yang paling umum dan jelas adalah kompatibilitas ke belakang. Di sini saya akan menyertakan penggunaan perpustakaan C pihak ketiga (kasus saya dengan JNI) dan penyediaan API C untuk implementasi C ++.
  • SFINAE . Di sini, sangat berguna bahwa dalam C ++ fungsi variabel tidak harus memiliki nama argumen, dan ketika menyelesaikan fungsi yang kelebihan beban, fungsi variabel dianggap sebagai yang terakhir (jika ada setidaknya satu argumen). Dan seperti fungsi lainnya, fungsi variabel hanya dapat dideklarasikan, tetapi tidak pernah dipanggil.

    Contoh
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static void detect(const U&); static int detect(...); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; }; 

    Meskipun dalam C ++ 14 Anda dapat melakukan sedikit berbeda.

    Contoh lain
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static constexpr bool detect(const U*) { return true; } template <class U> static constexpr bool detect(...) { return false; } public: static constexpr bool value = detect<T>(nullptr); }; 

    Dan dalam hal ini sudah perlu untuk menonton dengan apa argumen detect(...) dapat dipanggil. Saya lebih suka mengubah beberapa baris dan menggunakan alternatif modern untuk fungsi variabel, tanpa semua kekurangannya.

Templat varian atau cara membuat fungsi dari sejumlah argumen arbitrer di C ++ modern


Gagasan templat variabel diajukan oleh Douglas Gregor, Jaakko Jรคrvi dan Gary Powell pada tahun 2004, yaitu 7 tahun sebelum penerapan standar C ++ 11, di mana templat variabel ini didukung secara resmi.Standar mencakup revisi ketiga proposal mereka, N2080 .

Sejak awal, templat variabel dibuat sehingga pemrogram memiliki kesempatan untuk membuat Fungsi tipe-aman (dan portabel!) Dari sejumlah argumen yang berubah-ubah. Tujuan lain adalah untuk menyederhanakan dukungan untuk templat kelas dengan sejumlah parameter variabel, tetapi sekarang kita hanya berbicara tentang fungsi variabel.

Templat variabel membawa tiga konsep baru ke C ++ [C ++ 17 17.5.3] :

  • parameter Template paket ( Template parameter pack ) - adalah template parameter, bukannya yang dimungkinkan untuk mentransfer (termasuk 0) jumlah argumen template;
  • paket parameter fungsi ( paket parameter fungsi ) - karenanya, ini adalah parameter fungsi yang mengambil (termasuk 0) jumlah argumen fungsi;
  • dan perluasan paket ( ekspansi paket ) adalah satu-satunya hal yang dapat dilakukan dengan paket parameter.

Contoh
 template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); } 

class ... Args โ€” , Args ... args โ€” , args... โ€” .

Daftar lengkap di mana dan bagaimana paket parameter dapat diperluas diberikan dalam Standar itu sendiri [C ++ 17 17.5.3 / 4] . Dan dalam konteks diskusi fungsi variabel, cukup untuk mengatakan bahwa:

  • paket parameter fungsi dapat diperluas ke daftar argumen fungsi lain
     template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); } 

  • atau ke daftar inisialisasi
     template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; } 

  • atau ke daftar tangkap lambda
     template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); } 

  • paket parameter fungsi lain dapat diperluas dalam ekspresi konvolusi
     template <class ... Args> int foo(Args ... args) { return (0 + ... + args); } 

    Konvolusi muncul di C ++ 14 dan bisa unary dan binary, kanan dan kiri. Deskripsi paling lengkap, seperti biasa, ada dalam Standar [C ++ 17 8.1.6] .
  • kedua jenis paket parameter dapat diperluas menjadi sizeof ... operator
     template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); } 


Dalam mengungkapkan paket elipsis eksplisit diperlukan untuk mendukung berbagai template ( pola ) pengungkapan dan untuk menghindari ambiguitas ini.

Sebagai contoh
 template <class ... Args> void foo() { using OneTuple = std::tuple<std::tuple<Args>...>; using NestTuple = std::tuple<std::tuple<Args...>>; } 

OneTuple โ€” ( std:tuple<std::tuple<int>>, std::tuple<double>> ), NestTuple โ€” , โ€” ( std::tuple<std::tuple<int, double>> ).

Contoh implementasi printf menggunakan templat variabel


Seperti yang telah saya sebutkan, templat variabel juga dibuat sebagai pengganti langsung untuk fungsi variabel C. Penulis templat ini sendiri mengusulkan versi yang sangat sederhana namun aman bagi jenisnya printf- salah satu fungsi variabel pertama dalam C.

printf on templates
 void printf(const char* s) { while (*s) { if (*s == '%' && *++s != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout << *s++; } } template <typename T, typename ... Args> void printf(const char* s, T value, Args ... args) { while (*s) { if (*s == '%' && *++s != '%') { std::cout << value; return printf(++s, args...); } std::cout << *s++; } throw std::runtime_error("extra arguments provided to printf"); } 

Saya curiga, maka pola enumerasi argumen variabel ini muncul - melalui panggilan rekursif fungsi yang kelebihan beban. Tapi saya masih lebih suka opsi tanpa rekursi.

printf pada template dan tanpa rekursi
 template <typename ... Args> void printf(const std::string& fmt, const Args& ... args) { size_t fmtIndex = 0; size_t placeHolders = 0; auto printFmt = [&fmt, &fmtIndex, &placeHolders]() { for (; fmtIndex < fmt.size(); ++fmtIndex) { if (fmt[fmtIndex] != '%') std::cout << fmt[fmtIndex]; else if (++fmtIndex < fmt.size()) { if (fmt[fmtIndex] == '%') std::cout << '%'; else { ++fmtIndex; ++placeHolders; break; } } } }; ((printFmt(), std::cout << args), ..., (printFmt())); if (placeHolders < sizeof...(args)) throw std::runtime_error("extra arguments provided to printf"); if (placeHolders > sizeof...(args)) throw std::runtime_error("invalid format string: missing arguments"); } 

Resolusi Kelebihan dan Fungsi Templat Variabel


Ketika menyelesaikan, fungsi variatif ini dianggap, setelah yang lain, sebagai standar dan paling tidak terspesialisasi. Tetapi tidak ada masalah dalam kasus panggilan tanpa argumen.

Contoh yang berlebihan
 #include <iostream> void foo(int) { std::cout << "Ordinary function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } template <class T> void foo(T) { std::cout << "Template function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); foo(2.0); foo(1, 2); return 0; } 

Peluncuran hasil
 $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function 

Ketika kelebihan beban diselesaikan, fungsi templat variabel hanya dapat mem-bypass fungsi variabel C (meskipun mengapa mencampurnya?). Kecuali - tentu saja! - panggilan tanpa argumen.

Panggilan tanpa argumen
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); return 0; } 

Peluncuran hasil
 $ ./test Template variadic function C variadic function 

Ada perbandingan dengan elipsis - fungsi yang sesuai hilang, tidak ada perbandingan dengan elipsis - dan fungsi templat lebih rendah daripada yang non-templat.

Catatan singkat tentang kecepatan fungsi templat variabel


Pada tahun 2008, Loรฏc Joly mengajukan proposal N2772 kepada Komite Standardisasi C ++ , di mana ia menunjukkan dalam praktiknya bahwa fungsi templat variabel bekerja lebih lambat daripada fungsi serupa, argumennya adalah daftar inisialisasi ( std::initializer_list). Dan meskipun ini bertentangan dengan pembenaran teoretis dari penulis itu sendiri, Joli mengusulkan untuk mengimplementasikannya std::min, std::maxdan std::minmaxtepatnya dengan bantuan daftar inisialisasi, dan bukan dengan templat variabel.

Namun sudah pada tahun 2009, bantahan muncul. Dalam tes Joli, "kesalahan serius" ditemukan (tampaknya, bahkan untuk dirinya sendiri). Tes baru (lihat di sini dan di sini) menunjukkan bahwa fungsi templat variabel masih lebih cepat, dan terkadang signifikan. Yang tidak mengherankan sejak itu daftar inisialisasi membuat salinan elemen-elemennya, dan untuk templat variabel Anda dapat menghitung banyak pada tahap kompilasi.

Namun demikian, dalam C ++ 11 dan standar berikutnya std::min, std::maxdan std::minmaxmerupakan fungsi templat biasa, sejumlah argumen arbitrer yang diteruskan melalui daftar inisialisasi.

Ringkasan dan kesimpulan singkat


Jadi, fungsi variabel C-style:

  • Mereka tidak tahu jumlah argumen mereka atau tipenya. Pengembang harus menggunakan bagian dari argumen ke fungsi untuk meneruskan informasi tentang sisanya.
  • Secara implisit meningkatkan jenis argumen yang tidak disebutkan namanya (dan yang terakhir disebutkan). Jika Anda melupakannya, Anda mendapatkan perilaku yang tidak jelas.
  • Mereka mempertahankan kompatibilitas ke belakang dengan C murni dan karena itu tidak mendukung lewat argumen dengan referensi.
  • Sebelum C ++ 11, argumen bukan dari tipe POD tidak didukung , dan karena C ++ 11, dukungan untuk tipe non-sepele diserahkan kepada kebijaksanaan kompilator. Yaitu Perilaku kode tergantung pada kompiler dan versinya.

Satu-satunya penggunaan fungsi variabel yang diizinkan adalah berinteraksi dengan C API dalam kode C ++. Untuk yang lainnya, termasuk SFINAE , ada fungsi templat variabel yang:

  • Ketahui jumlah dan jenis semua argumen mereka.
  • Ketik aman, jangan mengubah jenis argumen mereka.
  • Mereka mendukung lewat argumen dalam bentuk apa pun - dengan nilai, dengan pointer, dengan referensi, dengan tautan universal.
  • Seperti fungsi C ++ lainnya, tidak ada batasan pada tipe argumen.
  • ( C ), .

Fungsi templat variabel dapat lebih verbose dibandingkan dengan rekanan gaya-C dan kadang-kadang bahkan membutuhkan versi non-templat sendiri yang berlebihan (traversal argumen rekursif). Mereka lebih sulit untuk membaca dan menulis. Tetapi semua ini lebih dari dibayar dengan tidak adanya kekurangan yang terdaftar dan adanya keuntungan yang terdaftar.

Nah, kesimpulannya sederhana: fungsi variatif dalam gaya C tetap di C ++ hanya karena kompatibilitas ke belakang, dan mereka menawarkan berbagai pilihan untuk menembak kaki Anda. Dalam C ++ modern, sangat disarankan untuk tidak menulis yang baru dan, jika mungkin, tidak menggunakan fungsi variabel C yang ada. Fungsi templat variabel milik dunia C ++ modern dan jauh lebih aman. Gunakan itu.

Sastra dan Sumber



PS


Sangat mudah untuk menemukan dan mengunduh versi elektronik dari buku-buku yang disebutkan di internet. Tapi saya tidak yakin itu legal, jadi saya tidak memberikan tautan.

Source: https://habr.com/ru/post/id430064/


All Articles