Parameter ...
In den Sprachen C und C++ ist es möglich, bei der Parameterliste einer Funktion mittels dreier Auslassungspunkte ... die genaue Anzahl an erwarteten Argumenten sowie deren Typ offenzulassen. Solchen als variadisch
bezeichneten Funktionen können somit je nach Situation mehr oder weniger beliebige Argumente übergeben werden, ohne dass der Programmierer für jeden denkbar möglichen Fall eine neue Signatur ausprogrammieren müsste. Das bekannteste Beispiel einer variadischen Funktion ist printf:
|
|
Auf dieser Seite wird eine detailierte Beschreibung über die Anwendung variadischer Funktionen sowie einige Implementations-Beispiele geliefert. Für eine einfache Auflistung der benötigten Aufrufe wird auf die Beschreibung der stdarg-Bibliothek verwiesen.
Details
Eine variadische Funktion kann beliebig viele Argumente erwarten, jedoch muss in C und C++ für variadische Funktionen stets mindestens ein Parameter bei der Deklaration fix angegeben werden. Die variadischen Parameter (welche mit Auslassungspunkten ... angegeben werden) müssen stets als letzter Parameter deklariert sein.
Obschon die Möglichkeit beliebiger Argumente verlockend ist, sind variadische Funktionen nur sehr selten sinnvoll und haben den Nachteil, dass der Typ der übergebenen Argumente im Allgemeinen vom Compiler nicht geprüft werden kann. Üblicherweise weiss ein Programmierer sehr genau, wieviele und was für welche Argumente einer Funktion übergeben werden sollten, weswegen variadische Funktionen bis heute kaum verwendet werden und gar als verpönt gelten. Dennoch gibt es die eine oder andere Ausnahme, welche sich als praktisch erwiesen hat.
In C++ wurden neue Mechanismen wie Templates, Funktionsüberladung oder Standard-Werte für Parameter eingebaut, welche weitaus praktischer und sicherer sind als variadische Funktionen. Andere Sprachen haben ähnliche Mechanismen eingebaut oder verwenden beispielsweise Arrays, um eine beliebige Anzahl an (sowohl strict als auch loose typed) Argumenten zu übergeben.
Es sei darauf hingewiesen, dass nebst variadischen Funktionen auch variadische Makros existieren, welche jedoch vom Preprozessor verarbeitet werden.
Programmierung von variadischen Funktionen
Um variadische Funktionen auszuprogrammieren, muss als erstes die stdarg-Bibliothek eingebunden werden.
|
|
In der stdarg-Bibliothek werden einige Makros definiert, mit welchen die variadischen Argumente angesprochen werden können. Im folgenden Beispiel ermittelt eine Funktion das Maximum aller übergebenen Zahlen:
|
|
Zuerst wird eine Variable mit dem Typ va_list definiert. Mittels va_start wird die Liste sodann initialisiert, indem der Name des letzten Parameters angegeben wird, welcher sich vor den Auslassungspunkten ... befindet. Daraufhin gibt jeder Aufruf von va_arg das jeweils nächste variadische Argument mit einer Typangabe zurück. Am Ende muss die Liste mittels va_end abgeschlossen werden. Etwas detailierter sind diese Makros bei der stdarg-Bibliothek beschrieben.
Es ist zu bemerken, das der Typ va_list ein Typ wie jeder andere ist und somit Argumentenlisten selbst an weitere Funktionen übergeben werden können. Hier ein Beispiel einer variadischen Funktion, welche ihre Argumentenliste an eine vsnprintf-Funktion übergibt.
|
|
Es ist zu beachten, dass der Typ va_list theoretisch auch als Rückgatyp verwendet werden kann. Da die Argumentenliste jedoch augenblicklich bei der Rückgabe ihrerselben ungültig wird, ist eine solche Vorgehensweise, mit was für Absichten auch immer, sinnlos und auch gefährlich.
Typ-Unsicherheit
Variadische Argumenten können im Allgemeinen nicht durch den Compiler auf die korrekte Verwendung eines Typs geprüft werden. Dies bedeutet zum einen, dass innerhalb der variadischen Funktion der Programmierer die Verantwortung über die korrekte Verwendung von Typen übernehmen muss. Das Ansprechen einer Variablen mit dem falschen Typ kann verständlicherweise zu einem falschen Resultat führen.
|
|
Mittels variadischen Argumenten können jedoch nicht nur falsche Resultate innerhalb der variadischen Funktion auftreten. Da dem Compiler keinerlei Informationen über Typen zur Verfügung stehen, kann der Compiler auch keine const-safety mehr garantieren. Dadurch kann eine variadische Funktion ungewollt Werte der aufrufenden Funktion verändern:
|
|
Wenn die const-Variable zudem noch als static deklariert wäre, würde dies in modernen Systemen unweigerlich zu einem Programmabsturz führen, dessen Ursache möglicherweise erst nach tagelanger Fehlersuche klar wird.
Ermitteln der Anzahl Argumente
Nebst der Typ-Unsicherheit haben variadische Funktionen auch noch den Nachteil, dass die totale Anzahl der übergebenen Argumente innerhalb der Funktion nicht ohne weiteres ermittelt werden kann. Es gibt hierfür verschiedene Lösungen. Eine Lösung ist, dass die Anzahl an übergebenen Argumenten in einem der ersten Argumente übergeben wird. Häufig wird für eine variadische Funktion einfach ein Parameter (Beispielsweise count) deklariert, welcher bei Aufruf der Funktion explizit angegeben werden muss. Da variadische Funktionen sowieso stets mindestens einen fixen Parameter besitzen müssen, ist diese Lösung sehr verbreitet (siehe auch Beispiel oben).
Beim Beispiel von printf wird die Anzahl an erwarteten Argumenten anhand des Inhalts des übergebenen Strings ermittelt (Heutige Compiler haben gar printf-Prüfer eingebaut).
Es gäbe auch die Lösung, die Anzahl an erwarteten Parametern mittels einer globalen Variablen zu speichern. Dieser Ansatz ist jedoch sowohl verpönt als auch gefährlich, wenn es darum geht, Parallelverarbeitung zu betreiben.
Eine andere beliebte Lösung, um die Anzahl übergebener Argumente zu ermitteln, ist die Verwendung eines sogenannten Sentinels
(Englisches Wort für Wächter
). Ein Sentinel bezeichnet dasjenige Argument, ab dessen Position die restliche Anzahl an erwarteter Argumente bekannt ist. Sehr häufig wird das letzte Argument als das Sentinel verwendet. Dieses Sentinel-Argument wird bei der Übergabe mit einem vorgegebenen Wert gefüllt (sehr häufig NULL). Tritt innerhalb der variadischen Funktion bei der Abarbeitung der variadischen Argumente dieser Wert auf, so ist ab diesem Punkt klar, wieviele Argumente noch verfügbar sind.
Sentinels werden nicht nur für variadische Funktionen verwendet, siehe auch die Argumente der main-Funktion. Der Einsatz bei variadische Funktionen ist jedoch einigermassen verbreitet und so gibt es gar teilweise Compiler, die Sentinels erkennen können, siehe Attribute.
Duplizieren von Argumentenlisten
Solange variadische Argumente innerhalb einer Funktion nur ein mal angesprochen werden müssen, genügend die oben aufgelisteten Makros vollauf. Für den Fall, dass eine Argumentenliste mehrfach abgearbeitet werden soll, existiert jedoch das Makro va_copy(dst,src), welches den Zeiger auf eine Argumentenliste von einer Variablen src in eine zweite Variable dst kopiert. Hier ein einfaches Beispiel, wie eine Argumentenliste mehrfach abgearbeitet wird:
|
|
Ein solches Duplizieren wird hauptsächlich benötigt, wenn ein Teil oder gar die ganze Argumentenliste an eine weitere Funktion weitergeleitet werden soll, beziehungsweise eine Argumentenliste als Eingabeparameter der auszuprogrammierenden Funktion erwartet wird. Spätestens bei der Verwendung des Typs va_list als Übergabeparameter wird ein Programmierer möglicherweise auf unerklärliche Phänomene im Zusammenhang mit variadischen Argumentenlisten stossen. Oftmals genügt es dann, die Argumentenliste zu duplizieren.
Für ein besseres Verständnis dafür, wie unerklärliche Phänomene auftreten beziehungsweise vermieden werden können, empfielt der Autor, sich zu überlegen, wie variadische Funktionen überhaupt funktionieren.
Wie funktionieren variadische Funktionen?
Variadische Funktionen sind in C und C++ möglich, da diese Sprachen erlauben, die Argumente von rechts nach links auf dem Stack abzulegen. Für mehr Informationen über den Aufbau von Stcks, wird auf die Seite über den Call-Stack verwiesen.
Das wichtigste, was ein Programmierer zum Verständnis von variadischen Funktionen wissen muss, ist, dass das Argument (von egal wievielen), welches beim Funktionsaufruf-Operator zuerst (am weitesten links) aufgelistet wurde, sich innerhalb der aufgerufenen Funktion stets unmittelbar beim Stackpointer befindet. Da mindestens ein Argument fix sein muss, kann während der Laufzeit mithilfe des letzten fixen Parameters vor den variadischen Parametern die Position der variadischen Argumente relativ zum Stackpointer ermittelt werden. Durch zusätzliche Angabe des zu erwartenden Typs jedes einzelnen folgenden variadischen Arguments können alle Argumente nacheinander ermittelt werden, indem der Stack Schritt für Schritt rückwärts abgearbeitet wird. Dies ist sowohl betreffend der Laufzeit, als auch betreffend des Speicherplatzes höchst effizient.
Die variadischen Argumente sind somit nur durch Angabe des letzten fixen Parameters erreichber. Rein technisch wäre es zwar grundsätzlich möglich, einen Mechanismus bereitszustellen, welcher variadische Funktionen ohne fixe Parameter abarbeiten könnte, doch ist dies nicht im Sprachumfang vorgesehen. Es existiert kein spezielles Keyword oder eine anderweitig syntaktische Speziallösung für das erste variadische Argument. Da zudem die Implementation je nach System leicht variieren kann, sollten variadische Argumente stets mithilfe der in der stdarg-Bibliothek definierten Makros angesprochen werden.
Leider kommt es je nach System vor, dass gewisse Makros nicht definiert sind. Im Allgemeinen ist davon abzuraten, Standardmakros bei nicht-Vorhandensein einfach selbst du definieren. Tatsächlich ist dem Autor jedoch keine andere Lösung bekannt. Da ein Programmierer früher oder später über dieses Hindernis stolpern wird, hier somit die Definition des Makros va_copy, wie es vermutlich auf den allermeisten Systemen funktionieren sollte:
|
Die anderen Makros sollten (durch Einbinden der stdarg-Bibliothek) auf jeden Fall verfügbar sein. Um jedoch die Neugierde des Lesers sowie des Autors selbst zu stillen, hier eine Auflistung der vararg-Makros, wie sie möglicherweise umgesetzt sein könnten. Der Leser darf meine Lösung gerne durchdenken und ausprobieren, von einer Verwendung ist jedoch abzuraten.
|