C++ WYKŁAD 13 ( ) Algorytmy
S PIS TREŚCI Funktory i predykaty Funkcje lambda
F UNKTORY I PREDYKATY Obiekt funkcyjny to obiekt, w którym jest zdefiniowany operator wywołania funkcji operator(). Zalety obiektów funkcyjnych: posiadają stan (pamięć), mają własny typ (mogą być parametrami szablonów), działają co najmniej tak szybko jak wskaźniki do funkcji. Funktor to obiekt klasy z operatorem wywołania funkcji. Predykat to funktor, który wyniku zwraca wartość boolowską.
O BIEKT FUNKCYJNY JAKO KRYTERIUM SORTOWANIA class Person { public: string firstname() const; string lastname() const; … }; class PersonSortCriterion { public: bool operator() (const Person &p1, const Person &p2) const { // a person is less than another person - if the last name is less // - if the last name is equal and the first name is less return p1.lastname()<p2.lastname() || p1.lastname()==p2.lastname() && p1.firstname()<p2.firstname(); } };
O BIEKT FUNKCYJNY ZE STANEM WEWNĘTRZNYM class IntSequence { private: int value; public: // constructor IntSequence (int initialValue = 0) : value(initialValue) {} // ‘‘function call’’ int operator() () { return ++value; } };
A LGORYTM FOR _ EACH Algorytm for_each aplikuje funkcje zdefiniowaną w obiekcie funkcyjnym do wszystkich elementów kolekcji. Algorytm for_each zwraca swój obiekt funkcyjny. Przykład: class MeanValue { int num; // number of elements long sum; // sum of all element values public: MeanValue () : num(0), sum(0) {} void operator() (int elem) { ++num; // increment count sum += elem; // add value } double value () { return static_cast (sum) / num; } }; // … vector coll = /* … */; MeanValue mv = for_each(coll.begin(), coll.end(), MeanValue()); cout << mv.value() << endl;
P REDEFINIOWANE OBIEKTY FUNKCYJNE Arytmetyczne obiekty funkcyjne: negate<>, plus<>, minus<>, multiplies<>, divides<>, modulus<>. Obiekty funkcyjne porównujące: less<> (domyślne kryterium przy sortowaniu czy wyszukiwaniu binarnym), greater<>, less_equal<>, greater_equal<>, equal_to<>, not_equal_to<>. Obiekty funkcyjne do tworzenia wyrażeń logicznych: logical_not<>, logical_and<>, logical_or<>.
F UNKCJE LAMBDA Programista często chciałby zdefiniować predykatowe funkcje w pobliżu wywołań takich funkcji, jak na przykład pochodzących ze standardowej biblioteki (szczególnie sort i find ) – oczywistym rozwiązaniem jest zdefiniowanie w takim miejscu funkcji lambda (określanej też jako lambda-wyrażenie). Funkcje lambda to anonimowe obiekty funkcyjne. Lambdy nie posiadają ani konstruktora domyślnego ani operatora przypisania. Główne zastosowanie funkcji lambda to ich użycie jako argumentu sterującego obliczeniami w innych funkcjach. Przykład: [](int x, int y) { return x + y; }
F UNKCJE LAMBDA Funkcja lambda określa typ zwracanego wyniku za pomocą frazy -> TYP. Przykład: [](int x, int y) -> int { int z = x * x; return z + y + 1; } Jeśli ciało funkcji lambda składa się z jednej instrukcji return, to typ zwracanego wyniku będzie wydedukowany za pomocą decltype() (możne wtedy pominąć frazę -> TYP ). Przykład: [](int x, int y) // -> decltype(x*x+y+1) { return x * x + y + 1; }
F UNKCJE LAMBDA Dostęp do lokalnych zmiennych lub pól w obiekcie określa się w funkcji lambda za pomocą domknięcia, czyli wewnątrz początkowych nawiasów kwadratowych [] na początku definicji. Domknięcie puste [] oznacza, że funkcja lambda nie potrzebuje dostępu do zmiennych z lokalnego środowiska (zdefiniowanych poza funkcją lambda). Domknięcie [&] oznacza, że wszystkie zmienne z lokalnego środowiska są dostępne przez referencję. Domknięcie [=] oznacza, że wszystkie zmienne z lokalnego środowiska są dostępne przez wartość (kopiowanie wartości następuje w miejscach, w których funkcja lambda odwołuje się do zewnętrznych zmiennych). W domknięciu można umieścić listę zmiennych zewnętrznych, z których funkcja lambda może korzystać, na przykład: int x, y; // … [x, &y] (…) { return …; }
F UNKCJE LAMBDA Można utworzyć obiekt funkcyjny anonimowego typu reprezentujący lambdę : auto lambda = [](…)->…{ … }; Do takiej lamdy można się potem odwołać jak do funkcji: lambda(…); Przykład: vector v {9, 4, 1, 6, 8}; bool sensitive = true; // … auto lambda = [sensitive](int x, int y) { return sensitive ? x<y : abs(x)<abs(y); } sort(v.begin(), v.end(), lambda);
S PIS TREŚCI Kompilacja i łączenie Moduły Biblioteki Biblioteka statyczna Biblioteka współdzielona Biblioteka dynamiczna Tworzenie bibliotek Nazwy zewnętrzne
K OMPILACJA I ŁĄCZENIE Plik jako jednostka kompilacji. Preprocesing – obsługa makr i dyrektyw włączających – dostarcza kompilatorowi jednostkę translacji. Kompilator analizuje jednostkę translacji w izolacji od reszty programu. Fizyczna struktura programu (podział na pliki) powinna wynikać z logicznej struktury programu. Rola linkera przy budowaniu programu albo biblioteki.
M ODUŁY Każdy większy program składa się z pewnej liczby oddzielnych części – modułów. Moduł to kompletny fragment programu (moduł obliczeniowy, moduł we/wy, moduł prezentacji, itp). Podział kodu na moduły porządkuje logikę programu. Należy minimalizować zależności między modułami.
B IBLIOTEKI Moduły, z których może korzystać wiele programów umieszcza się w oddzielnych skompilowanych plikach, zwanych bibliotekami. Typy bibliotek w C++: biblioteka statyczna jest dołączana do programu wynikowego na etapie linkowania; biblioteka współdzielona jest dołączana do programu w trakcie ładowania programu do pamięci; biblioteka dynamiczna jest dołączana do uruchomionego procesu w trakcie działania programu.
B IBLIOTEKI Biblioteka to zbiór klas, funkcji i zmiennych, z których mogą korzystać różne programy. Biblioteka ma postać binarną – jej poszczególne fragmenty są skompilowane (biblioteka jest kolekcją plików obiektowych). Korzystanie z bibliotek ułatwia programowanie (korzystamy z gotowych i sprawdzonych fragmentów kodu) i przyspiesza proces rekompilacji.
B IBLIOTEKI Wynikiem samej kompilacji pliku źródłowego ( plik.c albo plik.cpp ) jest plik plik.o pod Linuxem albo plik.obj pod Windowsem. Biblioteki statyczne mają nazwy libmodul.a pod Linuxem albo modul.lib pod Windowsem. Biblioteki dynamiczne mają nazwy libmodul.so pod Linuxem (tak jak biblioteki współdzielone) albo modul.dll pod Windowsem.
B IBLIOTEKA STATYCZNA Używając biblioteki statycznej przekazujemy jej archiwum linkerowi w czasie kompilacji. Linker wyszukuje w nim tylko tych plików obiektowych, które są niezbędne do działania naszego programu i kopiuje je bezpośrednio do programu. Program wynikowy korzystający z biblioteki statycznej jest obszerniejszy ale szybciej się ładuje do pamięci. Program wynikowy zlinkowany z biblioteką statyczną jest niezależny od plików zewnętrznych. Uaktualnienie biblioteki wymaga rekompilacji programu.
B IBLIOTEKA STATYCZNA lib.cpp lib.o static library prog.cpp prog.o a.out memory linker loader g++ar g++ ssh
B IBLIOTEKA WSPÓŁDZIELONA Programy korzystające biblioteki współdzielonej nie zawierają bezpośrednio kodu z tej biblioteki a tylko referencję do niej. Program wynikowy korzystający z biblioteki współdzielonej jest chudszy ale wolniej ładuje się do pamięci (biblioteki współdzielone są odszukiwane i ładowane do pamięci razem z programem). Program wynikowy skompilowany z biblioteką współdzieloną jest zależny od plików zewnętrznych. Zmodyfikowanie biblioteki współdzielonej spowoduje zmianę w działaniu programu ale bez jego ponownej kompilacji.
B IBLIOTEKA WSPÓŁDZIELONA lib.cpp lib.o shared library prog.cpp prog.o a.out memory linker loader g++ ssh
B IBLIOTEKA DYNAMICZNA Programy korzystające bibliotek dynamicznych nie zawierają bezpośrednio kodu z tej biblioteki ale muszą korzystać ze specjalnych metod włączania takich bibliotek w trakcie działania programu (plik nagłówkowy ). Program wynikowy korzystający z biblioteki dynamicznej jest chudszy i szybciej ładuje się do pamięci, ale działa wolniej (biblioteki dynamiczne można załadować w dowolnym momencie w trakcie działania programu). Program wynikowy skompilowany z biblioteką dynamiczną jest zależny od plików zewnętrznych. Zmodyfikowanie biblioteki dynamicznej spowoduje zmianę w działaniu programu ale bez jego ponownej kompilacji.
B IBLIOTEKA DYNAMICZNA lib.cpp lib.o dynamic library prog.cpp prog.o a.out memory linker loader g++ ssha.out
T WORZENIE BIBLIOTEK ( POD L INUXEM ) Tworzenie programu bez dołączanych bibliotek. Załóżmy, że mamy pliki src1.cpp, src2.cpp i src3.cpp, które stanowią moduł obliczeniowy oraz plik prog.cpp, który będzie korzystał z funkcji i klas zdefiniowanych w module obliczeniowym. Aby skompilować cały program razem z modułem obliczeniowym należy wydać polecenie: $ g++ -Wall -ansi -pedantic src1.cpp src2.cpp src3.cpp prog.cpp -o calculation Aby skompilować cały program razem z modułem obliczeniowym i statycznie zlinkować z innymi bibliotekami (rozmiar programu wynikowego będzie znacznie większy) należy wydać polecenie: $ g++ -static … Aby uruchomić skompilowany program należy wydać polecenie: $./calculation Aby sprawdzić z jakimi bibliotekami jest linkowany program i jakie symbole są w nim użyte należy wydać polecenie: $ ldd calculation $ nm calculation
T WORZENIE BIBLIOTEK ( POD L INUXEM ) Program korzystający z biblioteki statycznej. Najpierw kompilujemy pliki źródłowe do plików obiektowych: $ g++ -c -Wall -ansi -pedantic src1.cpp src2.cpp src3.cpp Następnie łączymy pliki obiektowe do jednego pliku bibliotecznego libsrc.a : $ ar src libsrc.a src1.o src2.o src3.o Na koniec należy skompilować plik z programem i zlinkować go z biblioteką: $ g++ -c -Wall -ansi -pedantic prog.cpp $ g++ -o calculation prog.o –L. –lsrc Teraz można uruchomić skompilowany program: $./calculation Wyjaśnienie: opcja -L ścieżka określa ścieżkę do biblioteki, opcja -l biblioteka określa nazwę biblioteki.
T WORZENIE BIBLIOTEK ( POD L INUXEM ) Program korzystający z biblioteki współdzielonej. Najpierw kompilujemy pliki źródłowe z opcją -fpic do plików obiektowych: $ g++ -fpic –c -Wall -ansi -pedantic src1.cpp src2.cpp src3.cpp Następnie łączymy pliki obiektowe do jednego pliku bibliotecznego libsrc.so : $ g++ –fpic -shared -o libsrc.so src1.o src2.o src3.o Na koniec należy skompilować plik z programem i wlinkować do niego informacje o bibliotece: $ g++ -Wall -ansi -pedantic prog.cpp $ g++ -o calculation prog.o –L. –lsrc Przed uruchomieniem programu trzeba zapisać w zmiennej środowiskowej LD_LIBRARY_PATH ścieżkę do biblioteki: $ export LD_LIBRARY_PATH= " LD_LIBRARY_PATH: ścieżka " Teraz można uruchomić skompilowany program: $./calculation
T WORZENIE BIBLIOTEK ( POD L INUXEM ) Program korzystający z biblioteki dynamicznej. Bibliotekę dynamiczną przygotowujemy tak samo jak bibliotekę współdzieloną libsrc.so : $ g++ -fpic –c -Wall -ansi -pedantic src1.cpp src2.cpp src3.cpp $ g++ –fpic -shared -o libsrc.so src1.o src2.o src3.o Na koniec należy skompilować plik z programem i dołączyć do niego dynamicznego linkera opcją -ldl : $ g++ -Wall -ansi -pedantic prog.cpp $ g++ -o calculation prog.o –ldl Teraz można uruchomić skompilowany program: $./calculation Wyjaśnienie: aby skorzystać z dynamicznego linkera należy do programu włączyć plik nagłówkowy ; aby załadować dynamiczną bibliotekę trzeba skorzystać z funkcji dlopen, dlsym, dlerror i dlclose.
N AZWY ZEWNĘTRZNE Nazwa jest łączona zewnętrznie jeśli można jej używać w jednostkach translacji innej niż ta, w której ją zdefiniowano. Nazwę zewnętrzną deklaruje się za pomocą słowa extern. Funkcja wbudowana musi być zdefiniowana w każdej jednostce translacji za pomocą identycznej definicji; ta sama reguła odnosi się do funkcji i klas szablonowych.
N OWOŚCI Z C++11 – SZABLONY ZEWNĘTRZNE I ALIASY SZABLONÓW Kompilator języka C++ musi stworzyć instancję szablonu zawsze kiedy napotka w pełni określony szablon w jednostce translacyjnej. W starszej wersji C++ nie było możliwości wstrzymania tworzenia instancji szablonu w takiej sytuacji. Obecnie wprowadzono ideę szablonów zewnętrznych w celu zablokowania tworzenia instancji w jednostce translacyjnej. Przykład: extern template class std::vector ; Należy zagwarantować, aby w jakiejś innej jednostce translacji instanja tego typu była utworzona. W języku C++ można używać aliasów dla szablonów. Przykład: template using vect = std::vector >;