Alles begann wie immer mit einem Fehler. Dies ist das erste Mal, dass ich mit der
Java Native Interface gearbeitet habe, und im C ++ - Teil habe ich eine Funktion eingeschlossen, die ein Java-Objekt erstellt. Diese Funktion -
CallVoidMethod
- ist variabel, d.h. Neben einem Zeiger auf die
JNI- Umgebung, einem Zeiger auf den zu erstellenden Objekttyp und einem Bezeichner für die aufgerufene Methode (in diesem Fall den Konstruktor) werden eine beliebige Anzahl anderer Argumente verwendet. Welches ist logisch, weil Diese anderen Argumente werden auf der Java-Seite an die aufgerufene Methode übergeben, und die Methoden können unterschiedlich sein, mit einer unterschiedlichen Anzahl von Argumenten eines beliebigen Typs.
Dementsprechend habe ich auch meinen Wrapper variabel gemacht. Um eine beliebige Anzahl von Argumenten an
CallVoidMethod
zu
CallVoidMethod
va_list
, da dies in diesem Fall anders ist. Ja, das hat
va_list
an
CallVoidMethod
gesendet. Und ließ den banalen JVM-Segmentierungsfehler fallen.
In 2 Stunden habe ich es geschafft, mehrere Versionen der JVM vom 8. bis zum 11. zu testen, weil: Erstens ist dies meine erste Erfahrung mit der
JVM , und in dieser Angelegenheit habe ich StackOverflow mehr vertraut als mir selbst und zweitens jemandem dann habe ich auf StackOverflow in diesem Fall empfohlen, nicht OpenJDK, sondern OracleJDK und nicht 8, sondern 10 zu verwenden. Und erst dann habe ich endlich bemerkt, dass es zusätzlich zur Variablen
CallVoidMethod
gibt, die eine beliebige Anzahl von Argumenten über
va_list
CallVoidMethodV
.
Was mir an dieser Geschichte nicht am besten gefallen hat, war, dass ich den Unterschied zwischen den Auslassungspunkten (Auslassungspunkten) und
va_list
nicht sofort bemerkt habe. Und nachdem ich es bemerkt hatte, konnte ich mir nicht erklären, was der grundlegende Unterschied war. Wir müssen uns also mit Auslassungspunkten und
va_list
und (da wir immer noch über C ++ sprechen) mit variablen Vorlagen befassen.
Was ist mit den Auslassungspunkten und der va_list, die im Standard angegeben sind?
Der C ++ - Standard beschreibt nur die Unterschiede zwischen seinen Anforderungen und denen von Standard C. Die Unterschiede selbst werden später erörtert, aber im Moment werde ich kurz erklären, was Standard C sagt (beginnend mit C89).
Warum? Aber weil!
Es gibt nicht viele Arten in C. Warum ist
va_list
im Standard deklariert, aber nichts wird über seine interne Struktur gesagt?
Warum brauchen wir eine Ellipse, wenn eine beliebige Anzahl von Argumenten an eine Funktion über
va_list
? Man könnte jetzt sagen: "als syntaktischer Zucker", aber vor 40 Jahren gab es sicher keine Zeit für Zucker.
Philip James Plauger
Phillip James Plauger in dem Buch
The Standard C Library - 1992 - sagt, dass C ursprünglich ausschließlich für PDP-11-Computer erstellt wurde. Und dort war es möglich, alle Argumente der Funktion mit einfacher Zeigerarithmetik zu sortieren. Das Problem trat mit der Popularität von C und der Übertragung des Compilers auf andere Architekturen auf. In der ersten Ausgabe von
The C Programming Language von Brian Kernighan und Dennis Ritchie - 1978 - heißt es ausdrücklich:
Übrigens gibt es keine akzeptable Möglichkeit, eine tragbare Funktion mit einer beliebigen Anzahl von Argumenten zu schreiben, weil Es gibt keine tragbare Möglichkeit für die aufgerufene Funktion, herauszufinden, wie viele Argumente beim Aufruf an sie übergeben wurden. ... printf
, die typischste C-Sprachfunktion einer beliebigen Anzahl von Argumenten, ... ist nicht portierbar und muss für jedes System implementiert werden.
Dieses Buch beschreibt
printf
, hat aber noch kein
vprintf
und erwähnt nicht den Typ und die Makros
va_*
. Sie erscheinen in der zweiten Ausgabe der C-Programmiersprache (1988), und dies ist das Verdienst des Komitees für die Entwicklung des ersten C-Standards (C89, auch bekannt als ANSI C). Das Komitee fügte dem
<stdarg.h>
Überschrift
<stdarg.h>
hinzu, die als Grundlage
<varargs.h>
von Andrew Koenig mit dem Ziel erstellt wurde, die Portabilität des UNIX-Betriebssystems zu
<varargs.h>
.
va_*
wurde beschlossen,
va_*
als Makros zu
va_*
, damit vorhandene Compiler den neuen Standard leichter unterstützen können.
Mit dem Aufkommen von C89 und der
va_*
-Familie ist es nun möglich geworden, tragbare variable Funktionen zu erstellen. Und obwohl die interne Struktur dieser Familie noch in keiner Weise beschrieben ist und keine Anforderungen dafür bestehen, ist bereits klar, warum.
Aus purer Neugier finden Sie Beispiele für die Implementierung von
<stdarg.h>
. Die gleiche „C-Standardbibliothek“ bietet beispielsweise ein Beispiel für
Borland Turbo C ++ :
<stdarg.h> von 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
Das viel neuere
SystemV ABI für AMD64 verwendet diesen Typ für
va_list
:
va_list von SystemV ABI AMD64 typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1];
Im Allgemeinen können wir sagen, dass der Typ und die Makros
va_*
eine Standardschnittstelle zum Durchlaufen von Argumenten einer variablen Funktion darstellen und ihre Implementierung aus historischen Gründen vom Compiler, den Zielplattformen und der Architektur abhängt. Darüber hinaus erschien in C eine Ellipse (d. H. Variable Funktionen im Allgemeinen) früher als
va_list
(d. H. Der Header
<stdarg.h>
). Und
va_list
wurde nicht erstellt, um die Auslassungspunkte zu ersetzen, sondern um Entwicklern das Schreiben ihrer tragbaren Variablenfunktionen zu ermöglichen.
C ++ behält weitgehend die Abwärtskompatibilität mit C bei, daher gilt alles oben Genannte für C. Es gibt aber auch Funktionen.
Variable Funktionen in C ++
Die
WG21- Arbeitsgruppe war an der Entwicklung des C ++ - Standards beteiligt. 1989 wurde der neu geschaffene C89-Standard zugrunde gelegt, der sich allmählich änderte, um C ++ selbst zu beschreiben. 1995
erhielt John Micco den Vorschlag
N0695 , in dem der Autor vorschlug, die Beschränkungen für Makros
va_*
:
- Weil Im Gegensatz zu C können Sie in C ++ die
register
von Variablen abrufen. Das zuletzt genannte Argument einer Variablenfunktion kann diese Speicherklasse haben.
- Weil Die in C ++ angezeigten Links verstoßen gegen die ungeschriebene Regel der C-Variablenfunktionen - die Größe des Parameters muss mit der Größe seines deklarierten Typs übereinstimmen -, dann kann das zuletzt genannte Argument kein Link sein. Ansonsten vages Verhalten.
- Weil In C ++ gibt es kein Konzept, „ den Typ des Arguments standardmäßig zu erhöhen “ , sondern die Phrase
Wenn der Parameter parmN
mit ... parmN
wird, einem Typ, der nicht mit dem Typ kompatibel ist, der nach Anwendung der Standardargument-Promotions resultiert, ist das Verhalten undefiniert
muss ersetzt werden durchWenn der Parameter parmN
mit ... parmN
wird, einem Typ, der nicht mit dem Typ kompatibel ist, der beim Übergeben eines Arguments entsteht, für das es keinen Parameter gibt, ist das Verhalten undefiniert
Ich habe nicht einmal den letzten Punkt übersetzt, um meinen Schmerz zu teilen. Erstens bleibt die "
Eskalation des
Standardargumenttyps " in C ++ Standard
[C ++ 17 8.2.2 / 9] . Und zweitens habe ich lange über die Bedeutung dieses Satzes nachgedacht, verglichen mit Standard C, wo alles klar ist. Erst nachdem ich N0695 gelesen hatte, verstand ich endlich: Ich meine das Gleiche.
Alle 3 Änderungen wurden jedoch übernommen
[C ++ 98 18.7 / 3] . Zurück in C ++ ist die Anforderung, dass eine Variablenfunktion mindestens einen benannten Parameter haben muss (in diesem Fall können Sie nicht auf die anderen zugreifen, aber dazu später mehr), verschwunden, und die Liste der gültigen Typen unbenannter Argumente wurde durch Zeiger auf Klassenmitglieder und
POD- Typen ergänzt.
Der C ++ 03-Standard brachte keine Änderungen an den Variationsfunktionen. C ++ 11 begann, ein unbenanntes Argument vom Typ
std::nullptr_t
in
void*
zu konvertieren, und erlaubte Compilern nach eigenem Ermessen, Typen mit nicht trivialen Konstruktoren und Destruktoren zu unterstützen
[C ++ 11 5.2.2 / 7] . C ++ 14 erlaubte die Verwendung von Funktionen und Arrays als zuletzt genanntem Parameter
[C ++ 14 18.10 / 3] , und C ++ 17 verbot die Verwendung der Erweiterung des Parameterpakets (
Pack-Erweiterung ) und der vom Lambda erfassten Variablen
[C ++ 17 21.10.1 / 1] .
Infolgedessen fügte C ++ seinen Fallstricken variable Funktionen hinzu. Nur nicht spezifizierte Typunterstützung mit nicht trivialen Konstruktoren / Destruktoren lohnt sich. Im Folgenden werde ich versuchen, alle nicht offensichtlichen Merkmale variabler Funktionen in einer Liste zusammenzufassen und durch spezifische Beispiele zu ergänzen.
Wie man variable Funktionen einfach und falsch benutzt
- Es ist falsch, das zuletzt genannte Argument mit einem heraufgestuften Typ zu deklarieren, d. H.
char
, signed char
, unsigned char
, singed short
, unsigned short
oder float
. Das Ergebnis gemäß dem Standard ist undefiniertes Verhalten.
Ungültiger Code void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
Von allen Compilern, die ich zur Hand hatte (gcc, clang, MSVC), gab nur clang eine Warnung aus.
Clang Warnung ./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
Und obwohl sich der kompilierte Code in allen Fällen korrekt verhalten hat, sollten Sie sich nicht darauf verlassen.
Es wird richtig sein void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
- Es ist falsch, das zuletzt genannte Argument als Referenz zu deklarieren. Beliebiger Link. Der Standard verspricht auch in diesem Fall undefiniertes Verhalten.
Ungültiger Code 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 hat diesen Code ohne einen einzigen Kommentar kompiliert. lang 6.0.0 hat eine Warnung ausgegeben, diese aber dennoch kompiliert.
Clang Warnung ./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
In beiden Fällen hat das Programm korrekt funktioniert (zum Glück können Sie sich nicht darauf verlassen). Aber MSVC 19.15.26730 zeichnete sich aus - es weigerte sich, den Code zu kompilieren, weil va_start
Argument va_start
keine Referenz sein.
Fehler von 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
Nun, die richtige Option sieht zum Beispiel so aus void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
- Es ist falsch,
va_arg
, den Typ char
, short
oder float
va_arg
erhöhen.
Ungültiger Code #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; }
Es ist hier interessanter. gcc bei der Kompilierung gibt eine Warnung aus, dass double
anstelle von float
muss. Wenn dieser Code weiterhin ausgeführt wird, wird das Programm mit einem Fehler beendet.
Gcc Warnung ./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
In der Tat stürzt das Programm mit einer Beschwerde über eine ungültige Anweisung ab.
Eine Dump-Analyse zeigt, dass das Programm ein SIGILL-Signal empfangen hat. Und es zeigt auch die Struktur von va_list
. Für 32 Bit ist dies
va = 0xfffc6918 ""
d.h. va_list
ist nur char*
. Für 64 Bit:
va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}}
d.h. genau das, was in SystemV ABI AMD64 beschrieben ist.
clang at compilation warnt vor undefiniertem Verhalten und schlägt außerdem vor, float
durch double
ersetzen.
Clang Warnung ./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; ^~~~~
Aber das Programm stürzt nicht mehr ab, die 32-Bit-Version produziert:
1 0 1073741824
64 Bit:
1 0 3
MSVC liefert genau die gleichen Ergebnisse, nur ohne Vorwarnung, auch mit /Wall
.
Hier könnte angenommen werden, dass der Unterschied zwischen 32 und 64 Bit auf die Tatsache zurückzuführen ist, dass im ersten Fall der ABI alle Argumente über den Stapel an die aufgerufene Funktion weiterleitet und im zweiten Fall die ersten vier (Windows) oder sechs (Linux) Argumente über die Prozessorregister, der Rest durch Stapel [ Wiki ]. Aber nein, wenn Sie foo
nicht mit 4 Argumenten, sondern mit 19 aufrufen und auf dieselbe Weise ausgeben, ist das Ergebnis dasselbe: Volles Durcheinander in der 32-Bit-Version und Nullen für alle float
in der 64-Bit-Version. Das heißt, Der Punkt ist natürlich in ABI, aber nicht in der Verwendung von Registern, um Argumente zu übergeben.
Natürlich ist es richtig, dies zu tun 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); }
- Es ist falsch, eine Instanz einer Klasse mit einem nichttrivialen Konstruktor oder Destruktor als unbenanntes Argument zu übergeben. Es sei denn natürlich, das Schicksal dieses Codes reizt Sie zumindest ein wenig mehr als "hier und jetzt kompilieren und ausführen".
Ungültiger Code #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; }
Clang ist der strengere von allen. Er weigert sich einfach, diesen Code zu kompilieren, da das zweite Argument, va_arg
kein POD-Typ ist, und warnt, dass das Programm beim Start va_arg
.
Clang Warnung ./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); ^
So wird es sein, wenn Sie immer noch mit dem -Wno-non-pod-varargs
.
MSVC warnt davor, dass die Verwendung von Typen mit nicht trivialen Konstruktoren in diesem Fall nicht portierbar ist.
Warnung von MSVC d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840: "Bar"
Der Code wird jedoch kompiliert und ordnungsgemäß ausgeführt. Folgendes wird in der Konsole angezeigt:
Ergebnis starten Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
Das heißt, Eine Kopie wird nur zum Zeitpunkt des Aufrufs von va_arg
, und das Argument wird, wie sich herausstellt, als Referenz übergeben. Es ist irgendwie nicht offensichtlich, aber der Standard erlaubt.
gcc 6.3.0 wird ohne einen einzigen Kommentar kompiliert. Die Ausgabe ist die gleiche:
Ergebnis starten Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
gcc 7.3.0 warnt auch vor nichts, aber das Verhalten ändert sich:
Ergebnis starten 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
Das heißt, Diese Version des Compilers übergibt Argumente nach Wert, und beim Aufruf erstellt va_arg
eine weitere Kopie. Es würde Spaß machen, diesen Unterschied beim Wechsel von der 6. zur 7. Version von gcc zu suchen, wenn die Konstruktoren / Destruktoren Nebenwirkungen haben.
Übrigens, wenn Sie explizit einen Verweis auf die Klasse übergeben und anfordern:
Ein weiterer falscher Code 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; }
dann werfen alle Compiler einen Fehler aus. Gemäß Standard.
Wenn Sie wirklich wollen, ist es im Allgemeinen besser, Argumente per Zeiger zu übergeben.
So 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; }
Überlastauflösung und variable Funktionen
Einerseits ist alles einfach: Der Abgleich mit einer Ellipse ist schlechter als der Abgleich mit einem regulären benannten Argument, selbst bei einer Standard- oder benutzerdefinierten Typkonvertierung.
Überlastungsbeispiel #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; }
Ergebnis starten $ ./test Ordinary function Ordinary function C variadic function
Dies funktioniert jedoch nur, bis der Aufruf von
foo
ohne Argumente separat betrachtet werden muss.
Rufen Sie foo ohne Argumente auf #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; }
Compiler-Ausgabe ./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() ^~~
Alles entspricht dem Standard: Es gibt keine Argumente - es gibt keinen Vergleich mit den Auslassungspunkten, und wenn die Überlastung behoben ist, wird die Variationsfunktion nicht schlechter als gewöhnlich.
Wann lohnt es sich trotzdem, variable Funktionen zu verwenden?
Nun, variative Funktionen verhalten sich manchmal nicht sehr offensichtlich und können sich im Kontext von C ++ leicht als schlecht portabel herausstellen. Im Internet gibt es viele Tipps wie "Erstellen oder verwenden Sie keine variablen C-Funktionen", aber sie werden ihre Unterstützung nicht aus dem C ++ - Standard entfernen. Diese Funktionen bieten also einige Vorteile? Na da.
- Der häufigste und offensichtlichste Fall ist die Abwärtskompatibilität. Hier werde ich sowohl die Verwendung von C-Bibliotheken von Drittanbietern (mein Fall mit JNI) als auch die Bereitstellung der C-API für die C ++ - Implementierung einbeziehen.
- SFINAE . Hier ist es sehr nützlich, dass in C ++ eine Variablenfunktion keine benannten Argumente haben muss und dass beim Auflösen überladener Funktionen eine Variablenfunktion als letzte betrachtet wird (wenn mindestens ein Argument vorhanden ist). Und wie jede andere Funktion kann eine variable Funktion nur deklariert, aber nie aufgerufen werden.
Beispiel 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; };
Obwohl in C ++ 14 können Sie etwas anders machen.
Ein weiteres Beispiel 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); };
Und in diesem Fall ist es bereits notwendig zu beobachten, mit welchen Argumenten detect(...)
aufgerufen werden kann. Ich würde es vorziehen, ein paar Zeilen zu ändern und eine moderne Alternative zu variablen Funktionen zu verwenden, ohne all ihre Mängel.
Variantenvorlagen oder das Erstellen von Funktionen aus einer beliebigen Anzahl von Argumenten in modernem C ++
Die Idee variabler Vorlagen wurde bereits 2004 von Douglas Gregor, Jaakko Järvi und Gary Powell vorgeschlagen, d. H. 7 Jahre vor der Einführung des C ++ 11-Standards, in dem diese variablen Vorlagen offiziell unterstützt wurden. Der Standard enthielt eine dritte Überarbeitung ihres Vorschlags,
N2080 .
Von Anfang an wurden variable Vorlagen erstellt, damit Programmierer aus einer beliebigen Anzahl von Argumenten typsichere (und portable!) Funktionen erstellen konnten. Ein weiteres Ziel ist es, die Unterstützung für Klassenvorlagen mit einer variablen Anzahl von Parametern zu vereinfachen. Jetzt geht es jedoch nur noch um variable Funktionen.Variable Vorlagen brachten drei neue Konzepte in C ++ [C ++ 17 17.5.3] :- Template - Parameter - Paket ( Template - Parameter - Pack ) - ist eine Parametervorlage, statt dessen ist es möglich , jede (einschließlich 0) Anzahl der Template - Argument zu übertragen;
- ein Paket von Funktionsparametern ( Funktionsparameterpaket ) - dementsprechend ist dies ein Funktionsparameter, der eine beliebige (einschließlich 0) Anzahl von Funktionsargumenten akzeptiert;
- und die Erweiterung des Pakets ( Pack-Erweiterung ) ist das einzige, was mit dem Parameterpaket durchgeführt werden kann.
Beispiel template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); }
class ... Args
— ,
Args ... args
— ,
args...
— .
Eine vollständige Liste, wo und wie Parameterpakete erweitert werden können, finden Sie im Standard selbst [C ++ 17 17.5.3 / 4] . Und im Zusammenhang mit der Diskussion variabler Funktionen genügt es zu sagen:Das Funktionsparameterpaket kann in die Argumentliste einer anderen Funktion erweitert werden template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); }
oder zur Initialisierungsliste template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; }
oder zur Lambda-Erfassungsliste template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); }
Ein weiteres Paket von Funktionsparametern kann in einem Faltungsausdruck erweitert werden template <class ... Args> int foo(Args ... args) { return (0 + ... + args); }
Faltungen erschienen in C ++ 14 und können unär und binär sein, rechts und links. Die vollständigste Beschreibung findet sich wie immer im Standard [C ++ 17 8.1.6] .
Beide Arten von Parameterpaketen können in den Operator sizeof ... erweitert werden template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); }
das explizite Auslassungs Pakets Bei der Angabe der benötigt wird , um die verschiedenen Vorlagen (zur Unterstützung Muster ) Offenlegung und diese Mehrdeutigkeit zu vermeiden.Zum Beispiel 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>>
).
Beispielimplementierung von printf mit variablen Vorlagen
Wie bereits erwähnt, wurden Variablenvorlagen auch als direkter Ersatz für die Variablenfunktionen von C erstellt. Die Autoren dieser Vorlagen selbst schlugen ihre sehr einfache, aber typsichere Version vor printf
- eine der ersten Variablenfunktionen in C.printf auf Vorlagen 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"); }
Ich vermute, dann erschien dieses Muster der Aufzählung variabler Argumente - durch einen rekursiven Aufruf überladener Funktionen. Aber ich bevorzuge immer noch die Option ohne Rekursion.printf auf Vorlagen und ohne Rekursion 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"); }
Überlastungsauflösung und variable Vorlagenfunktionen
Bei der Auflösung werden diese variativen Funktionen nach anderen als Standard und am wenigsten spezialisiert betrachtet. Bei einem Aufruf ohne Argumente gibt es jedoch kein Problem.Überlastungsbeispiel #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; }
Ergebnis starten $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function
Wenn die Überladung behoben ist, kann eine variable Vorlagenfunktion nur eine variable C-Funktion umgehen (obwohl warum sie mischen?). Außer - natürlich! - Aufruf ohne Argumente.Aufruf ohne Argumente #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; }
Ergebnis starten $ ./test Template variadic function C variadic function
Es gibt einen Vergleich mit einer Ellipse - die entsprechende Funktion verliert, es gibt keinen Vergleich mit einer Ellipse - und die Vorlagenfunktion ist der Nicht-Vorlagenfunktion unterlegen.Ein kurzer Hinweis zur Geschwindigkeit variabler Vorlagenfunktionen
Im Jahr 2008 reichte Loïc Joly seinen Vorschlag N2772 beim C ++ Standardization Committee ein , in dem er in der Praxis zeigte, dass variable Vorlagenfunktionen langsamer arbeiten als ähnliche Funktionen, deren Argument die Initialisierungsliste ist ( std::initializer_list
). Und obwohl dies im Gegensatz zu theoretischer Fundierung des Autors selbst, schlug Joly zu implementieren std::min
, std::max
und std::minmax
es ist durch die Initialisierung Listen statt Variante Muster.Aber bereits im Jahr 2009 erschien eine Widerlegung. Bei den Tests von Joli wurde ein „schwerwiegender Fehler“ entdeckt (es scheint sogar für sich selbst). Neue Tests (siehe hier und hier) haben gezeigt, dass variable Vorlagenfunktionen immer noch schneller und manchmal erheblich sind. Was seitdem nicht verwunderlich ist Die Initialisierungsliste erstellt Kopien ihrer Elemente, und für variable Vorlagen können Sie bei der Kompilierung viel zählen.Doch in C ++ 11 und nachfolgenden Standards std::min
, std::max
und std::minmax
- das ist die üblichen Template - Funktionen, eine beliebige Anzahl von Argumenten, die durch die Initialisierung Liste übertragen werden.Kurze Zusammenfassung und Schlussfolgerung
Variablenfunktionen im C-Stil:- Sie kennen weder die Anzahl ihrer Argumente noch ihre Typen. Der Entwickler muss einen Teil der Argumente an die Funktion verwenden, um Informationen über den Rest zu übergeben.
- Erhöhen Sie implizit die Typen unbenannter Argumente (und die zuletzt genannten). Wenn Sie es vergessen, bekommen Sie vages Verhalten.
- Sie behalten die Abwärtskompatibilität mit reinem C bei und unterstützen daher nicht die Übergabe von Argumenten als Referenz.
- Vor C ++ 11 wurden Argumente, die nicht vom Typ POD waren, nicht unterstützt , und seit C ++ 11 lag die Unterstützung für nicht triviale Typen im Ermessen des Compilers. Das heißt, Das Verhalten des Codes hängt vom Compiler und seiner Version ab.
Die einzig zulässige Verwendung von Variablenfunktionen ist die Interaktion mit der C-API in C ++ - Code. Für alles andere, einschließlich SFINAE , gibt es variable Vorlagenfunktionen, die:- Kennen Sie die Anzahl und Art aller ihrer Argumente.
- Geben Sie safe ein und ändern Sie nicht die Typen ihrer Argumente.
- Sie unterstützen die Übergabe von Argumenten in jeder Form - nach Wert, Zeiger, Referenz, universeller Verknüpfung.
- Wie bei jeder anderen C ++ - Funktion gibt es keine Einschränkungen für die Argumenttypen.
- ( C ), .
Variable Vorlagenfunktionen können im Vergleich zu ihren Gegenstücken im C-Stil ausführlicher sein und erfordern manchmal sogar eine eigene überladene Nicht-Vorlagenversion (rekursives Durchlaufen von Argumenten). Sie sind schwerer zu lesen und zu schreiben. All dies wird jedoch durch das Fehlen der aufgeführten Mängel und das Vorhandensein der aufgeführten Vorteile mehr als bezahlt.Die Schlussfolgerung ist einfach: Variative Funktionen im C-Stil bleiben nur aufgrund der Abwärtskompatibilität in C ++ erhalten und bieten eine Vielzahl von Optionen, um Ihr Bein zu schießen. In modernem C ++ ist es sehr ratsam, keine neuen zu schreiben und wenn möglich keine vorhandenen variablen C-Funktionen zu verwenden. Variable Vorlagenfunktionen gehören zur Welt des modernen C ++ und sind viel sicherer. Verwenden Sie sie.Literatur und Quellen
PS
Es ist einfach, elektronische Versionen der im Internet genannten Bücher zu finden und herunterzuladen. Ich bin mir jedoch nicht sicher, ob dies legal ist, daher gebe ich keine Links.