PROGRAMOWANIE GENERYCZNE W JĘZYKU C++ Krzysztof Rogala Instytu Automatyki i Informatyki Stosowanej Wydział Elektroniki i Technik Informacyjnych Politechniki Warszawskiej
CZYM JEST PROGRAMOWANIE GENERYCZNE? Tworzenie ogólnego kodu, który może byd ukonkretniany w przyszłości. Ukonkretnienie polega na: określeniu typów danych, na których kod operuje, podaniu dodatkowych parametrów algorytmu.
SZABLONY JĘZYKA C++ Szablony klas: template<typename T, int N> class Tablica { T m_tablica[n]; Szablony funkcji: template<typename T> bool less(const T& arg1, const T& arg2) { return arg1 < arg2; Ukonkretnienie: Tablica<int, 10> tablicaint; if(less(10, 12)) printf("10 < 12");
STL STANDARD TEMPLATE LIBRARY Standardowa biblioteka, definiująca m.in. ogólne kontenery i algorytmy, np.: std::set - posortowany zbiór zawierający elementy bez powtórzeo: template<typename T> struct ComplexComparator { bool operator()(const std::complex<t>& c1, const std::complex<t>& c2) { if(c1.real() == c1.real()) return c1.imag() < c2.imag(); else return c1.real() < c2.real(); std::set<std::complex<float>, ComplexComparator<float> > complexset; przykład algorytmu kopiowanie sekwencji: std::set<std::complex<float>, ComplexComparator<float> > complexset1; std::set<std::complex<float>, ComplexComparator<float> > complexset2; std::copy(complexset1.begin(), complexset1.end(), complexset2.begin());
CIEKAWSZY PRZYKŁAD Funkcja wypisująca zawartośd dowolnego kontenera, przechowującego dowolny typ: template<typename CONT> void print(const CONT& cont) { typedef typename std::iterator_traits<cont::iterator>::value_type VT; std::copy(cont.begin(), cont.end(), std::ostream_iterator<vt>(std::cout, " ")); Przykład użycia: std::set<int> intset; intset.insert(1); intset.insert(2); intset.insert(3); print(intset);
AUTOMATYCZNE GENEROWANIE KODU 1 boost::bind dotychczasowy sposób wykonywania operacji na elementach kontenera: struct FunctionObject { void operator()(int x) { x += 5; std::set<int> intset; std::set<int>::iterator iter1 = intset.begin(); auto iter2 = --intset.end(); FunctionObject fn; std::for_each(iter1, iter2, fn); natomiast z wykorzystaniem boost::bind: void fun(int x, int y) { x += y; std::for_each(iter1, iter2, boost::bind(fun, _1, 5));
AUTOMATYCZNE GENEROWANIE KODU 2 wyrażenia lambda Prosty przykład generowania nienazwanej funkcji: [](int x, int y) -> int { int z = x + y; return z + x; Wyrażenie to generuje następujący kod: class XXX { public: int operator()(int x, int y) { int z = x + y; return z; następnie zwracany jest obiekt utworzonego typu (jego nazwa jest nieznana).
WYRAŻENIA LAMBDA I STL Ponieważ wyrażenie lambda zwraca obiekt, można go urzyd jako parametr algorytmu z STL. Przykład pokazuje obliczanie sumy wszystkich elementów zbioru: std::set<int> intset; int total = 0; std::for_each ( somelist.begin(), somelist.end(), [&](int x) { total += x; ); Warto zauważyd, że cały kod generowany jest podczas kompilacji, a zatem nie powoduje żadnych nakładów na czas wykonania. Stosowanie wyrażeo lambda i boost::bind znacznie spowalnia czas kompilacji, natomiast nie ma żadnego wpływu na czas wykonania programu. Jedyną istotną różnicą jest fakt, że obiekty tworzone są jedynie na czas wywołania algorytmu. auto myfunobj = [&total](int x) { total += x;
WERYFIKACJA POPRAWNOŚCI KONCEPTÓW Koncept: wzorzec definiujący sposób implementowania klas, wykonujących konkretne zadania. Przykład: Iterator Koncept dwukierunkowego iteratora określa, że jest to klasa implementująca podstawowe operacje typowe dla wskaźnika: operatory ++ i --, operator * i ->. Przykład z STL: void foo() { std::list<int> intlist; std::stable_sort(intlist.begin(), intlist.end()); return 0; Powyższy kod nie skompiluje się, jednak z komunikatów o błędzie nie wynika na czym polega błąd.
Błąd polega na tym, że w klasie std::list żaden dostępny iterator nie umożliwia swobodnego dostępu do elementów (nie implementuje operacji przesunięcia wskaźnika o więcej niż jedną pozycję). Brak ten można wykryd następująco: template <class RandomAccessIterator> void stable_sort_constraints(randomaccessiterator i) { typename std::iterator_traits<randomaccessiterator>::difference_type n; i += n; Skompilowanie tego kodu wygeneruje czytelny komunikat o błędzie, informujący o braku możliwości wykonania danej operacji. Celem jest skompilowanie powyższego kodu lecz nie wykonywanie go, ponieważ zazwyczaj nie ma on sensu lub zabiera dużo czasu. Aby to osiągnąd deklarowany i definiowany jest wskaźnik do stworzonej funkcji, który jednak nigdzie nie jest wywoływany.
METAPROGRAMOWANIE Metaprogramowanie to w ogólności tworzenie programów, które modyfikują kod innych (lub swój własny) programów. W przypadku metaprogramowania przy pomocy szablonów C++ sprowadza się to do tworzenia kodu rozwiązującego problemy na etapie kompilacji. Standardowy przykład obliczanie silnii: template<int n> struct Silnia { enum { wynik = Silnia<n-1>::wynik * n //konkretyzacja dla warunku stopu, template<> struct Silnia<0> { enum { wynik = 1 Wywołanie: int x = Silnia<12>::val; Spowoduje obliczenie (w czasie kompilacji) wartości silnii z 12.
PĘTLE I INSTRUKCJE WARUNKOWE Każdą pętle można zapisd przy pomocy rekurencji, a zatem: template<int N, int P> struct Potega { enum { val = N*Potega<N, P-1>::val template<int N> struct Potega<N, 0> { enum { val = 1 Powyższy kod pozwala na obliczanie dodatnich potęg liczb całkowitych. Instrukcje warunkowe można tworzyd wykorzystująd trójargumentowy operator :? template<int n1, int n2> struct Max { enum { val = n1 > n2? n1 : n2
Powyższy przykład można zapisad bardziej ogólnie: template<bool condition> struct Select { template<> struct Select<true> { static int getfirst(int n1, int n2) { return n1; static int f(int n1, int n2) { return getfirst(n1, n2); template<> struct Select<false> { static int getsecond(int n1, int n2) { return n2; static int f(int n1, int n2) { return getsecond(n1, n2); template<bool condition> void maximum(int n1, int n2) { return Select<(n1 > n2)>::f(n1,n2); Użytecznośd tych metod jest jednak ograniczona. Ponieważ obliczenia wykonywane mają byd w czasie kompilacji, wykonywanie obliczeo na danych podanych przez użytkownika nie jest trywialne.
CIEKAWOSTKI Standard C++ 0x umożliwia stosowanie technik programistycznych dostępnych wcześniej wyłącznie dla klas i funkcji nieszablonowych dla szablonów. Są to m.in: Klasy lub funkcje szablonowe oznaczone jako extern Udostępniono słowo kluczowe analogiczne do typedef, using, działające dla nieukonkretnionych szablonów. Umożliwiono tworzenie szablonów o zmiennej liczbie parametrów: template<typename... Values> struct Tuple { /* */ Implementacja klasy Tuple (klasa przechowująca n wartości dowolncyh typów) bazuje na prywatnym dziedziczeniu rekurencyjnym: template<class First, typename... Rest> class Tuple : private Tuple<Rest> { private: First m_element;