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:
- 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.
- 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.
- Funkcja max zwraca zawsze referencje do GreaterThanComparable, więc konieczne jest rzutowanie na typ wynikowy (tu Int).