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).
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 olehJika 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
- 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); }
- 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); }
- 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); }
- 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::max
dan std::minmax
tepatnya 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::max
dan std::minmax
merupakan 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.