11.5 Zmienna liczba argumentów

Zdarzają się funkcje, które nie mają w naturalny sposób określonej, z góry znanej liczby argumentów. Jako przykład możemy wyobrazić sobie funkcję wyznaczająca maksimum z dwóch, trzech, czterech... liczb, albo drukującą wartości pewnej nieustalonej z góry ilości argumentów. W C/C++ istnieje sposób na tworzenie tego rodzaju funkcji, choć jest to nieco zawiłe i trudne w użyciu: można zastosować funkcje o zmiennej liczbie argumentów (ang. variable-length argument list). W C++ zwykle lepiej i prościej zastosować opisany dalej mechanizm przeciążania funkcji lub narzędzia z nowego standardu (np. tak zwane krotki lub listy inicjujące).

Funkcje tego typu deklarujemy ze znakiem wielokropka (' ...') w miejsce parametrów, których liczby nie specyfikujemy. Przed wielokropkiem wymienione są wszystkie „normalne”, obowiązkowe parametry. Na przykład deklaracja

       int fun(char* ...);
deklaruje funkcję, którą można wywołać z obowiązkowym pierwszym argumentem (typu char*) i dowolną liczbą dodatkowych argumentów. Tę samą deklarację można też zapisać z przecinkiem po ostatnim obowiązkowym argumencie:
       int fun(char*, ...);
Jak napisać treść funkcji aby prawidłowo odczytać w niej wszystkie przekazane argumenty? Przede wszystkim należy dołączyć plik nagłówkowy cstdarg a następnie, wewnątrz funkcji:
  1. Utworzyć zmienną typu va_list. Typ ten jest dostępny po dołączeniu pliku nagłówkowego cstdarg (nazwa va_list pochodzi od variable-arguments list). Załóżmy, że zmienna ta nazywa się ap (jest to tradycyjna nazwa takiej zmiennej, od argument pointer).
  2. Wywołać (tylko raz) funkcję va_start podając jako jej pierwszy argument utworzoną wcześniej zmienną typu va_list, a jako drugi argument zmienną odpowiadającą ostatniemu parametrowi obowiązkowemu funkcji; w poniższym przykładzie wywołanie ma zatem postać va_start(ap,typ).
  3. Wartości kolejnych argumentów odczytywać wywołując funkcję va_arg: jej pierwszym argumentem powinna być zmienna ap, a drugim nazwa typu tego argumentu funkcji który chcemy odczytać. Wynika z tego zatem, że ten typ trzeba znać! Trzeba też wiedzieć, kiedy skończyć wczytywanie argumentów. Najczęściej informacja o liczbie i typie argumentów w aktualnym wywołaniu jest w jakiś sposób przekazywana poprzez pierwsze, obowiązkowe argumenty funkcji.
  4. Po zakończeniu wczytywania argumentów wywołać funkcję va_end ze zmienną ap jako jedynym argumentem. Funkcja ta porządkuje stos; jeśli jej nie wywołamy, to program może się załamać.

Jako przykład rozpatrzmy program:


P67: varg.cpp     Funkcje o zmiennej liczbie argumentów

      1.  #include <iostream>
      2.  #include <cstdarg>
      3.  using namespace std;
      4.  
      5.  void typy(const char typ[] ...);
      6.  
      7.  int main() {
      8.      typy("SxS", "Jan", 0, "Maria");
      9.      typy("issD", 17, "Jan", "Maria", 1.);
     10.      typy("iDdsiI", 17, 19.5, 1.5, "OK", -1, 8);
     11.  }
     12.  
     13.  void typy(const char typ[] ...) {
     14.      int     i = 0, integ;
     15.      char    c, *strin;
     16.      double  doubl;
     17.  
     18.      va_list ap;
     19.  
     20.      va_start(ap,typ);
     21.  
     22.      while ( (c = typ[i++]) != '\0') {            
     23.          switch (c) {
     24.              case 'i':
     25.              case 'I':
     26.                  integ = va_arg(ap,int);
     27.                  cout << "Liczba int   : " << integ << endl;
     28.                  break;
     29.              case 'd':
     30.              case 'D':
     31.                  doubl = va_arg(ap,double);
     32.                  cout << "Liczba double: " << doubl << endl;
     33.                  break;
     34.              case 's':
     35.              case 'S':
     36.                  strin = va_arg(ap,char*);
     37.                  cout << "Napis        : " << strin << endl;
     38.                  break;
     39.              default:
     40.                  cout << "Nielegalny kod typu!!!!!" << endl;
     41.                  goto KONIEC;
     42.          }
     43.      }
     44.      KONIEC:
     45.      cout << endl;
     46.  
     47.      va_end(ap);                                  
     48.  }

Pierwszym, obowiązkowym argumentem funkcji typy jest napis, czyli tablica znaków, z których ostatni jest znakiem ' \0'. Kolejne znaki tego napisu określają typy kolejnych argumentów wywoływanej funkcji: 'd' lub 'D' — typ double, 'i' lub 'I' — typ int, 's' lub 'S' — typ char*, czyli wskaźnik do napisu. W ciele funkcji, po wykonaniu kroków 1 i 2 z podanego powyżej schematu postępowania, odczytujemy w pętli while kolejne argumenty. Najpierw () z napisu typ odczytujemy kolejny znak (aż będzie nim znak ' \0'). Znak ten określa typ kolejnego argumentu do wczytania. Znając ten typ, za pomocą instrukcji switch przechodzimy do wczytywania kolejnego argumentu: wczytujemy go do zmiennej roboczej o odpowiednim typie za pomocą wywołania funkcji va_arg (patrz punkt 3 schematu). Jeśli odczytany znak nie odpowiada żadnemu ze spodziewanych typów, wypisywany jest komunikat i za pomocą instrukcji goto przerywane jest wykonywanie zarówno bloku switch, jak i pętli while. Tym niemniej funkcja va_end musi nawet wtedy być wywołana (), aby umożliwić „posprzątanie” stosu i kontynuowanie programu. Przykład działania tego programu podany jest poniżej:

    Napis        : Jan
    Nielegalny kod typu!!!

    Liczba int   : 17
    Napis        : Jan
    Napis        : Maria
    Liczba double: 1

    Liczba int   : 17
    Liczba double: 19.5
    Liczba double: 1.5
    Napis        : OK
    Liczba int   : -1
    Liczba int   : 8
Przy pierwszym wołaniu powstaje błąd, gdyż użyta jest w napisie typ litera 'x', która nie odpowiada żadnemu typowi. Funkcja kończy swoje działanie, ale porządkuje stos i dalszy przebieg programu może być prawidłowy.

Ponieważ w deklaracji i definicji funkcji nie jest określony typ parametrów, wyłączona zostaje kontrola zgodności typów dla jej wywołań. Aby uprościć stosowanie funkcji o zmiennej liczbie parametrów, „krótkie” wartości całkowite awansowane są do typu int, a wartości float do typu double. Na przykład w drugim wywołaniu funkcji typy jednym z argumentówr jest '1.'. Gdybyśmy opuścili kropkę dziesiętną, literał odpowiadałby wartości całkowitej 1 i zostałby przekazany jako czterobajtowa wartość typu int. Ta zostałaby następnie odczytana jako double, a więc wartość ośmiobajtowa, co doprowadziłoby do trudno wykrywalnego błędu w programie.

Stosować funkcje o zmiennej liczbie argumentów należy tylko wtedy, gdy naprawdę są potrzebne i robić to trzeba bardzo ostrożnie.

W nowym standardzie C++11 istnieją inne, lepsze metody do przekazywania nieokreślonej z góry liczby danych do funkcji, o których powiemy w dalszej części.

T.R. Werner, 25 lutego 2017; 22:31