Podręcznik

1. Algorytmy uogólnione

1.1. Funkcje uogólnione bez szablonów

W praktyce programowania często spotykamy się z funkcjami (algorytmami), które można zastosować do szerokiej klasy typów i struktur danych. Typowym przykładem jest funkcja obliczająca maksimum dwu wartości. Ten trywialny, aczkolwiek przydatny kod można zapisać np. w postaci:


int max(int a,int b) {
  return (a>b)?a:b;
};

Funkcja max wybiera większy z dwu int-ów, ale widać, że kod będzie identyczny dla argumentów dowolnego innego typu pod warunkiem, iż istnieje dla niego operator porównania i konstruktor kopiujący. W językach programowania z silną kontrolą typów, takich jak C, C++ czy Java definiując funkcję musimy jednak podać typy przekazywanych parametrów oraz typ wartości zwracanej. Oznacza to, że dla każdego typu argumentów musimy definiować nową funkcję max:


int    max(int a, int b)      {return (a>b)?a:b;};
double max(double a,double b) {return (a>b)?a:b;};
string max(string a,string b) {return (a>b)?a:b;};
// skorzystaliśmy tu z dostępnej w C++ możliwości przeładowywania funkcji

main() {
  cout<< max(7,5)<< end;
  cout<< max(3.1415,2.71)<< endl;
  cout<< max("Ania","Basia")<< endl;
}

Takie powtarzanie kodu, poza oczywistym zwiększeniem nakładu pracy, ma inne niepożądane efekty, związane z trudnością zapewnienia synchronizacji kodu każdej z funkcji. Jeśli np. zauważymy błąd w kodzie, to musimy go poprawić w kilku miejscach. To samo dotyczy optymalizacji kodu. W powyższym przykładzie kod jest wyjątkowo prosty, ale nie jest trudno znaleźć przykłady, gdzie już taki prosty nie będzie.

Funkcje uogólnione bez szablonów

Jak radzili, a właściwie jak radzą sobie programiści bez możliwości skorzystania z szablonów? Tradycyjne sposoby rozwiązywania tego typu problemów to między innymi makra lub używanie wskaźników typów ogólnych, takich jak void *, jak np. w funkcji qsort ze standardowej biblioteki C:


#define max(a,b) ( (a>b)?a:b) )
void qsort(void *base, size_t nmemb, size_t size,
           int(*compar)(const void *, const void *));

Mimo iż użyteczne, żadne z tych rozwiązań nie może zostać uznane za wystarczająco ogólne i bezpieczne.

Stosowanie makr sprowadza się w dużej mierze do prostego mechanizmu "znajdź i zamień" - ze wszelkimi tego konsekwencjami, w tym niedostrzegalnym na pierwszy rzut oka problemem z priorytetami operatorów. Popatrzcie na poniższy przykład:


#define SQR(a) a*a
int y = SQR(x+2); // tzn x+2*x+2 !!!

Stosowanie rzutowania na void jest rozwiązaniem lepszym - ale tylko odrobinę. Zobaczcie że nawet w przykładzie qsort - zupełnie niepotrzebnie przekazuję pewne informacje (jak porównać dwie liczby, jaki jest rozmiar elementu tablicy, ile tablica ma elementów). No i pozbawiam się wsparcia ze strony kompilatora / środowiska w kontroli poprawności typów, argumentów, ip... ,

Można się również pokusić o próbę rozwiązania tego problemu za pomocą mechanizmów programowania obiektowego. W sumie jest to bardziej wyrafinowana odmiana rzutowania na void *. Polega na zdefiniowaniu ogólnego typu dla obiektów, które mogą być porównywane, i następnie korzystanie z obiektów pochodnych do nich.


class GreaterThenComparable {
public:
    virtual bool operator>( const GreaterThenComparable &b)  const = 0;
} ;

const GreaterThenComparable &max(const GreaterThenComparable &a,
                                   const GreaterThenComparable &b) {
        return (a>b)? a:b;
}

class Int:public GreaterThenComparable {
    int val;
public:
    Int(int i = 0):val(i){};
    operator int() {return val;};
    virtual bool operator>(const GreaterThenComparable &b) const {
    return val > static_cast< const Int& >(b).val;}
};

main() {
    Int a(3),b(2);
    Int c;
    c = (const Int &)::max(a,b);
    cout<< (int)c << endl;
}

Widać więc wyraźnie, że to podejście wymaga sporego nakładu pracy, a więc w szczególności w przypadku tak prostej funkcji jak max jest wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady:

  1. Wymaga dziedziczenia z abstrakcyjnej klasy bazowej GreaterThanComparable, czyli może być zastosowane tylko do typów zdefiniowanych przez nas. Inne typy, w tym typy wbudowane, wymagają kopertowania w klasie opakowującej, takiej jak klasa Int w powyższym przykładzie.
  2. Ponieważ potrzebujemy polimorfizmu funkcja operator>() musi być funkcją wirtualną, a więc musi być funkcją składową klasy i nie może być typu inline. W przypadku tak prostych funkcji niemożność rozwinięcia ich w miejscu wywołania może prowadzić do dużych narzutów w czasie wykonania.
  3. Funkcja max zwraca zawsze referencje do GreaterThanComparable, więc konieczne jest rzutowanie na typ wynikowy (tu Int).