czwartek, 27 maja 2010

Wielokropek w C++

Ostatnio pojawił się u mnie w kodzie C++ wielokropek (..., ang. ellipsis). Rzecz nie była zbyt jasna, więc dzisiaj więcej szczegółów ;)

Zastanawialiście się kiedyś jak działa funkcja printf? Ma ona w sobie pewną magię, bo przyjmuje niemalże dowolną liczbę parametrów - coś, czego nie robią funkcje na co dzień. Spójrzmy na jej definicję:

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

Pojawia się tutaj tajemniczy wielokropek :) Jak go użyć w swojej aplikacji? Najlepiej wcale nie używać ;)

Zastosowanie najlepiej pokazać na kawałku kodu :)

#include <cstdarg>
double average(int n, ...) {
  va_list list;
  va_start(list, n);
  double total = 0.0;
  for (int i = 0; i < n; i++) {
    total += va_arg(list, double);
  }
  va_end(list);
  return total / n;
}


Ciekawsze rzeczy pogrubiłem:

1. #include <cstdarg> - Aby móc korzystać z wielokropka potrzebny jest nagłówek standardowy cstdarg. Tzn. jest to nam potrzebne do definiowania funkcji, deklarowanie funkcji z wielokropkiem spokojnie się powiedzie bez tego ;) Nagłówek ten udostępnia nam następujące elementy:

2. (int n, ...) - wielokropek może wystąpić tylko jako ostatni "argument" funkcji. Dodatkowo w C nie może on być jednym argumentem funkcji. W C++ nie ma takiego ograniczenia, ale... patrz punkt 4 ;)

3. va_list list - "va" to skrót od "variable arguments". Typ va_list to lista argumentów przekazana z pomocą wielokropka i to na niej będziemy pracować.

4. va_start(list, n) - makro inicjujące listę argumentów. Ważny jest drugi parametr tej funkcji - jest to argument, który jest tuż przed wielokropkiem na liście argumentów. Nie musi to być liczba argumentów - chociaż jest sensowne, aby tak było ;) Jak widać, w C++ można napisać deklarację funkcji przyjmującej wyłącznie wielokropek (niezgodne z C) - ale nie napiszemy do niej już sensownej definicji (tzn. wykorzystującej zmienną listę argumentów)...

5. va_arg(list, double) - to makro przetwarza kolejny argument z listy argumentów (1 parametr), przyjmując że ma on typ taki jak podany jako drugi parametr. Jak widać nie można kontrolować kolejności, w jakiej argumenty będą przetwarzane przez to makro. Również "przetworzenie" większej liczby argumentów niż jest na liście lub podanie błędnego typu nie wzruszy tej funkcji - ona po prostu interpretuje kolejne bity pamięci jak podany typ danych.

6. va_end(list) - to makro musimy wywołać na liście argumentów, aby po sobie elegancko posprzątać ;)

Jak widać nie jest to trudne... Ale dlaczego by z tego nie korzystać? Wyobraźmy sobie taki kod:

#include <iostream>

using namespace std;

int main() {
  cout << average(6, 2.0, 3.0, 4.0, 7.0, 5.0, 8.0) << endl;
  cout << average(7, 2.0, 3.0, 4.0, 7.0, 5.0, 8.0) << endl;
  cout << average(6, 2.0, 3.0, 4.0, 7, 5.0, 8.0) << endl;
  return 0;
}


Dostaniemy 3 różne wyniki, przy czym tylko pierwszy jest poprawny :) Niestety kompilator milczy jeżeli popełnimy jakiś prosty błąd:

1. Podamy mniej argumentów niż funkcja się spodziewa (2. wywołanie) - wtedy va_arg> pobierze kolejne bity pamięci znajdujące się za argumentami (jakieś śmieci) i zinterpretuje po swojemu - nieprzewidywalne rezultaty.

2. Podamy inny typ argumentu niż jest spodziewany (3. wywołanie) - wtedy znowu va_arg>, nie patrząc na nic, pobierze bity i zinterpretuje po swojemu - co skończy się kolejnym nieprzewidywalnym rezultatem.

Dobrze, że w C# jest to już lepiej rozwiązane ;)

Brak komentarzy:

Prześlij komentarz