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 für jeden denkbar möglichen Fall eine neue Signatur ausprogrammiert werden müsste. Das bekannteste Beispiel einer variadischen Funktion ist printf:



Declaration


Hello
Hello 42
Hello 42
Hello World
Hello W0r1d
#include <stdio.h>

int printf(const char *, ...);

int main(){
  printf("Hello\n");
  printf("Hello %i\n",         42);
  printf("Hello %i%i\n",       4, 2);
  printf("Hello %s\n",         "World");
  printf("Hello %s%i%c%u%s\n", "W", 0, 'r', 1, "d");
  return 0;
}

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 zusätzlichen Parameter werden mit drei Auslassungspunkten ... angegeben und müssen stets als letzter Parameter deklariert sein. Die drei Auslassungspunkte werden im Englischen als ellipsis bezeichnet. Auf Deutsch entspricht dies dem Begriff Ellipse.

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. Diese Typ-Unsicherheit kann zu schwer auffindbaren Problemen führen, siehe unten. Üblicherweise ist jedoch sehr genau bekannt, 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überladungen oder Standard-Werte 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.

C
C++
#include <stdarg.h>
#include <cstdarg>

In der stdarg-Bibliothek werden einige Makros definiert, mit welchen die zusätzlichen Argumente angesprochen werden können. Im folgenden Beispiel ermittelt eine Funktion das Maximum aller übergebenen Zahlen:



Define list
Initialize list

Get next argument


Close list




441
int max(int count, ...){
  unsigned int curmax = 0;
  va_list argumentlist;
  va_start(argumentlist, count);
  while(count--){
    unsigned int arg = va_arg(argumentlist, unsigned int);
    if(arg > curmax){curmax = arg;}
  }
  va_end(argumentlist);
  return curmax;
}

int main(){
  printf("%i\n", max(5, 42, 365, 441, 253, 133));
  return 0;
}

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 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.


















1,2,3,4,5
#include <stdio.h>
#include <stdarg.h>

void createliststring(char* buffer, int count, ...){
  char formatstring[1024];
  char* fptr = formatstring;
  count--;
  while(count--){*fptr++='%';*fptr++='i';*fptr++=',';}
  *fptr++='%';*fptr++='i';*fptr++='\0';
  va_list argumentlist;
  va_start(argumentlist, count);
  vsnprintf(buffer, 1024, formatstring, argumentlist);
  va_end(argumentlist);
}

int main(){
  char buffer[1024];
  createliststring(buffer, 5, 1, 2, 3, 4, 5);
  printf("%s\n", buffer);
  return 0;
}

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

Die zusätzlichen Argumente von variadischen Funktionen 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 unbedingt auf die korrekte Verwendung von Typen geachtet werden muss. Das Ansprechen einer Variablen mit dem falschen Typ kann verständlicherweise zu einem falschen Resultat führen. Im folgenden Beispiel wird fälschlicherweise ein Pointer auf einen int übergeben, dieser innerhalb der Funktion jedoch als int ohne Pointer aufgefasst.




-1073743828
void stupidfunction(int dummy, ...){
  va_list argumentlist;
  va_start(argumentlist, dummy);
  printf("%i\n", va_arg(argumentlist, int));
  va_end(argumentlist);
}

int main(){
  int a = 1;
  stupidfunction(0, &a);
  return 0;
}

Ebenfalls gefährlich ist die Verwendung von Typen, welche vom Compiler mittels Typ-Promotion automatisch umgewandelt werden. Im folgenden Beispiel wird ein float übergeben, welcher durch den Compiler automatisch in einen double konvertiert wird:




BAD_INSTRUCTION
void badfunction(int dummy, ...){
  va_list argumentlist;
  va_start(argumentlist, dummy);
  printf("%f\n", va_arg(argumentlist, float));
  va_end(argumentlist);
}

int main(){
  float a = 1.f;
  badfunction(0, a);
  return 0;
}

Heutige Compiler warnen jedoch, wenn solche Promotionen auftreten. Die Lösung des float-Umwandlungs-Problems wäre, innerhalb der Funktion den zusätzlichen Parameter mittels double auszulesen und ihn danach wieder als float zu casten.


1.000000
float arg = (float)va_arg(argumentlist, double);
printf("%f\n", arg);

Eine kleine Anmerkung zu diesem letzten Beispiel: Dem aufmerksame Leser wird nicht entgangen sein, dass printf ebenfalls eine variadische Funktion ist. Das Casting des Wertes nach float ist somit überflüssig, da beim Aufruf von printf sofort wieder eine Promotion nach double stattfindet. Dies ist übrigens der Grund weswegen Werte vom Typ float oder double ohne zusätzliches Casting bei der printf-Funktion angegeben werden können.

Speziell Vorsicht geboten ist in diesem Zusammenhang mit dem Null-Pointer. Manche Compiler definieren diesen als den Integer-Wert 0, welcher somit als int in die Parameterübergabe einfliesst. Bei 64-Bit-Systemen kann dies jedoch verständlicherweise zu Problemen führen.

Da dem Compiler keinerlei Informationen über Typen zur Verfügung stehen, kann der Compiler bei variadischen Funktionen auch keine const-safety mehr garantieren. Dadurch kann eine variadische Funktion ungewollt Werte der aufrufenden Funktion verändern:















1

2
void dangerousfunction(int dummy, ...){
  va_list argumentlist;
  va_start(argumentlist, dummy);
  int* arg = va_arg(argumentlist, int*);
  va_end(argumentlist);
  *arg = 2;
}

void printInt(const int* i){
  printf("%i\n", *i);
}

int main(){
  const int a = 1;
  printInt(&a);
  dangerousfunction(0, &a);
  printInt(&a);
  return 0;
}

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.

Interessanter kleiner Einschub: Eine ältere Version obigen Beispiels verwendete anstelle von printInt direkt einen Aufruf von printf und irgendwann merkte der Autor, dass mit neueren Compilern das Resultat dieses bösen Beispiels tatsächlich konstant war, obschon es dies nicht sein dürfte. Offensichtlich nehmen Compiler bei const-Variablen mittlerweile Optimierungen vor, selbst wenn der Compiler angewiesen wird, keinerlei Optimierungen vorzunehmen. Konkret wurde der Inhalt der Variablen a in einem Register gespeichert und bei einem erneuten printf einfach nochmals übergeben. Durch den Aufruf einer weiteren Funktion printInt muss jedoch auch hier der Compiler das Zepter an die Grundprinzipien der Sprache übergeben.

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 zusätzlichen 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 die zusätzlichen Argumente einer variadischen 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:
















54321
void inverse(int count, ...){
  int i;
  va_list argumentlist1;
  va_list argumentlist2;
  va_start(argumentlist1, count);
  while(count--){
    va_copy(argumentlist2, argumentlist1);
    for(i=0; i<count; i++){va_arg(argumentlist2, int);}
    printf("%i", va_arg(argumentlist2, int));
    va_end(argumentlist2);
  }
  va_end(argumentlist1);
}

int main(){
  inverse(5, 1, 2, 3, 4, 5);
  return 0;
}

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 werden vermutlich unerklärliche Phänomene im Zusammenhang mit variadischen Funktionen auftreten. 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 Stacks, wird auf die Seite über den Call-Stack verwiesen.

Das wichtigste zum Verständnis von variadischen Funktionen 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 Auslassungspunkten ... die Position der zusätzlichen Argumente relativ zum Stackpointer ermittelt werden. Durch zusätzliche Angabe des zu erwartenden Typs jedes einzelnen folgenden 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 zusätzlichen Argumente einer variadischen Funktione 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 zusätzliche Argument. Da zudem die Implementation je nach System leicht variieren kann, sollten die zusätzlichen Argumente von variadischen Funktionen 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 während des Programmierens früher oder später über dieses Hindernis gestolpert werden wird, hier somit die Definition des Makros va_copy, wie es vermutlich auf den allermeisten Systemen funktionieren sollte:

#define va_copy(d,s) ((d)=(s))

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.

typedef char* va_list;
#define va_start(v,p) ((v)=(char*)(&(p)))
#define va_arg(v,t)   (((v)+=sizeof(t)),(*(t*)(v)))
#define va_end(v)     ((v)=0)