W C++11 kompilator generuje automatycznie dla przykładowej klasy Klasa: C++ 11: Metody usunięte i jawnie domyślne Konstruktor domyślny: Klasa() Konstruktor kopiujący: Klasa(const Klasa& k) Operator przypisania: operator=(const Klasa& k) Destruktor: ~Klasa() Konstruktor przenoszący: Klasa(const Klasa&& k) Operator przypisania przenoszący: operator=(const Klasa&& k) 33 Reguły tworzenia: 1. Jeżeli choć jeden konstruktor został utworzony jawnie, to żadne inne domyślne nie są tworzone 2. Jeżeli wirtualny destruktor został utworzony jawnie, to domyślny destruktor nie jest tworzony 3. Jeżeli konstruktor przenoszący lub operator przypisania przenoszący został utworzony jawnie, to nie są tworzone domyślnie konstruktor domyślny ani operator przypisania domyślny 4. Jeżeli jawnie stworzone zostały: konstruktor kopiujący lub przenoszący, operator przypisania zwykły lub przenoszący, lub destruktor, to nie są tworzone domyślnie konstruktor przenoszący ani operator przypisania przenoszący Reguły tworzenia: Te reguły rzutują na obecność składowych klas w przypadku dziedziczenia. Np. jeżeli klasa bazowa nie ma własnego konstruktora domyślnego, który mógłby zostać wywołany w klasie pochodnej, to w klasie pochodnej kompilator nie wygeneruje jej własnego konstruktora domyślnego. Dotychczas, aby jawnie zarządzać tworzeniem lub zakazem tworzenia konstruktorów stosowana była deklaracja w sekcji private: private: CMySingleton() { ~CMySingleton() { CMySingleton(const CMySingleton&); // Prevent copy-construction CMySingleton& operator=(const CMySingleton&); // Prevent assignment 34 35 Wady dotychczasowego rozwiązania: 1. Aby ukryć konstruktor kopiujący, trzeba go zadeklarować prywatnie, a skoro jest trzeba też zadeklarować domyślny, choćby nic nie robił, bo inaczej sam się nie utworzy. 2. Nawet jeżeli domyślny nic nie robi, to jest zadeklarowany, a w związku z tym daje większy koszt obliczeniowy wywołania, niż ten utworzony automatycznie przez kompilator. 3. Konstruktor kopiujący i operator przypisania są zadeklarowane prywatne, ale w metodach klasy friend oraz w metodach tej klasy można próbować je wywołać wygenerują błąd linkera (nie mają kodu). 4. Cała konstrukcja klasy dla kogoś, kto nie uważał na wykładach z PO i ZPO, jest niejasna i nieoczywista. Składnia w C++98: struct niekopiowalny { niekopiowalny() {; private: niekopiowalny(const niekopiowalny&); niekopiowalny& operator=(const niekopiowalny&); ; Składnia w C++11: struct niekopiowalny { niekopiowalny() =default; niekopiowalny(const niekopiowalny&) =delete; niekopiowalny& operator=(const niekopiowalny&) =delete; ; 36 37 1
Składnia w C++11 korzyści: 1. Generowanie konstruktora domyślnego nadal jest wstrzymane z racji deklaracji konstruktora kopiującego, ale.. można to zmienić korzystając z deklaracji =defaut. 2. Konstruktor kopiujący i operator kopiowania są publiczne, ale usunięte, więc przy próbie ich wywołania lub definiowania pojawi się błąd kompilacji. 3. Intencja autora kodu staje się czytelniejsza, nawet ktoś, kto nie uczęszczał na PO i ZPO, ma szansę się domyślić, co wolno a czego nie wolno używać. Określanie =default poza deklaracją klasy: struct widget { widget()=default; inline widget& operator=(const widget&); ; inline widget& widget::operator=(const widget&) =default; Konstruktor lub operator można wskazać jako default poza deklaracją klasy tylko pod warunkiem, że został zadeklarowany jako inline. 38 Wskazówka: ze względu na wyższą efektywność, tam gdzie to możliwe, należy zamieniać puste ciała konstruktorów i operatorów na polecenia =default 39 Blokowanie niechcianych wywołań: Blokowanie niechcianych wywołań: Jeżeli nie chcemy, aby obiekty danego typu były tworzone dynamicznie: struct widget { void* operator new(std::size_t) =delete; ; Jeżeli nie chcemy określonych konwersji argumentów w wywołaniach metod lub funkcji: void call_with_true_double_only(float) =delete; void call_with_true_double_only(double param) { return; Podanie argumentu typu int wywoła jego konwersję do double. Jeżeli nie chcemy żadnych konwersji argumentów w wywołaniach metod lub funkcji i chcemy aby argumenty jednego i tylko jednego typu były akceptowane: template < typename T > void call_with_true_double_only(t) =delete; void call_with_true_double_only(double param) {... return; 40 41 C++11: pętla for oparta na zakresie Nowa składnia pętli for for ( deklaracja : wyrażenie ) instrukcje wyrażenie tablica, obiekt implementujący semantykę kontenera (zwraca obiekt w stylu iteratora za pomocą metod begin i end) deklaracja zmiennej - typu takiego jak elementy w tablicy lub kontenerze instrukcje wykonywane tyle razy, ile elementów przechowuje tablica lub kontener C++ 11: Pętla for oparta na zakresie vector<int> vec; vec.push_back( 10 ); vec.push_back( 20 ); for (int i : vec ) { cout << i; UKSW, WMP. SNS, Warszawa 43 2
C++11: pętla for oparta na zakresie Przykłady Modyfikowanie zawartości kontenera: vector<int> vec; vec.push_back( 1 ); vec.push_back( 2 ); for (int& i : vec ) { i++; // inkrementuje wartości z wektora C++11: pętla for oparta na zakresie Obiekty posiadające zakres to: wszelkie kontenery z biblioteki STL, ponieważ: 1. posiadają iteratory, które wspierają metody operator*, operator!= i operator++ (zadeklarowane jako metody własne albo jako funkcje globalne), 2. posiadają metody begin i end (zadeklarowane jako metody własne albo jako funkcje globalne), zwracające iteratory na początek i koniec kontenera. wszelkie inne obiekty zaimplementowane samemu o funkcjonalności kontenerów STL (obowiązkowa w/w funkcjonalność). 44 45 Mechanizmy dedukcji typów danych Pochodzący C++98: 1. Szablony i ich parametry C++ 11: Dedukcja typów danych Wprowadzone w C++11: 1. auto 2. decltype Pozwalają uniknąć ciągłego wypisywania typów, które są oczywiste bądź powtarzają się. Zmiana typu zmiennej w jednym miejscu kodu propaguje się na cały kod (kompilator sam dedukuje nowy typ danych uwaga: może nas zaskoczyć). 47 Zasady dedukcji dla auto Podobne do zasad dedukcji dla parametrów szablonu z C++98 Deklarując auto jako typ zmiennej auto x = 10; auto odgrywa rolę taką, jak parametr T w szablonie: template <typename T> void fun(t x) ; fun(10); Stąd: const char name[] = "ABC"; // name: const char[13] auto arr1 = name; // arr1: const char* void Fun(int, double); // Fun: void(int, double) auto func1 = Fun; // func1: void (*)(int, double) 48 Pożytek z auto Mamy szablon funkcji, która na pewnym zakresie elementów kontenera identyfikowanym przez parę iteratorów, wykonuje czynności ważne i potrzebne : template<typename It> // wykonuje czynności ważne i potrzebne void czwip(it b, It e) // na rzecz elementów od 'b' do 'e' { for (; b!= e; ++b) { typename std::iterator_traits<it>::value_type currvalue = *b;... Odwołujemy się pracowicie do cech charakterystycznych iteratora, żeby pozyskać typ danych wskazywanych przez te iteratory. 49 3
Pożytek z auto Mamy szablon funkcji, która na pewnym zakresie elementów kontenera identyfikowanym przez parę iteratorów, wykonuje czynności ważne i potrzebne : template<typename It> // wykonuje czynności ważne i potrzebne void czwip(it b, It e) // na rzecz elementów od 'b' do 'e' { for (; b!= e; ++b) { auto currvalue = *b;... Wersja dla mniej pracowitych. Pożytek z auto Jeszcze jeden przykład dla mniej pracowitych : map<string, string> address_book; for ( auto address_entry : address_book ) { cout << address_entry.first << " < " << address_entry.second << ">" << endl; 50 51 Pożytek z auto Kiedy bezpieczniej jest pisać auto przykład: pozyskanie rozmiaru kontenera: std::vector<int> v;... unsigned sz = v.size(); Co prawda typ zwracany przez.size() to: vector<int>::size_type ale wszyscy wiedzą, że to typ całkowity bez znaku, więc spotyka się unsigned. Nie wszyscy jednak wiedzą, że choć w Win32 zmienne tych dwóch typów zajmują te samą liczbę bajtów, to już w Win64 nie (unsigned zajmuje mniej).. Rekompilacja kodu na Win64 powiedzie się, ale dla dostatecznie dużych zbiorów danych działanie może przestać być poprawne. Niespodzianki z auto mamy funkcję, która dla pewnej klasy widgetów zwraca wektor opisujący ich cechy, tj. co potrafią, a czego nie: std::vector<bool> cechy(const Widget& w); Kod programu: Widget w;... bool cecha5 = cechy(w)[5]; // czy posiada cechę nr 5.. A gdyby tak napisać: auto cecha5 = cechy(w)[5]; // czy posiada cechę nr 5 Dlatego prościej i bezpieczniej pisać: auto sz = v.size(); 52 Powinno działać tak samo. 53 Niespodzianki z auto Dla: auto cecha5 = cechy(w)[5]; Typ zmiennej cecha5 to vector<bool>::reference (klasa zagnieżdżona w vector<bool>). Niespodzianki z auto Dla: auto cecha5 = cechy(w)[5]; Typ zmiennej cecha5 to vector<bool>::reference (klasa zagnieżdżona w vector<bool>). Powód jest natury technicznej: klasa vector<bool> została tak zaprojektowana, aby przechowywać wartości w postaci spakowanej jeden bit na każdy element (a w C++ nie ma referencji do pojedynczych bitów). Stąd operator[] dla vector<bool> zwraca obiekt, który działa tak jak bool& Oczywiście, wśród jego wielu funkcjonalności, istnieje też możliwość konwersji tego obiektu do bool. Teraz zmienna cecha5 jest kopią obiektu przechowującego referencję do właściwej informacji bitu nr 5. Niestety, funkcja cechy zwraca referencję do obiektu, który za chwilę zniknie (funkcja cechy zwraca kontener obiekt tymczasowy, więc jego elementy też są tymczasowe..). Dlatego cecha5 będzie przechowywać referencję do obiektu, który nie istnieje. 54 55 4
Niespodzianki z auto Klasa: vector<bool>::reference to tzw. klasa proxy (proxy class). Takich klas jest wiele. Przestroga: należy unikać sytuacji w kodzie, gdzie: auto pewnazmienna = wyrażenie typu klasa proxy ; No tak, tylko że one zostały tak napisane, żeby być jak najmniej widoczne (w założeniu: mają być całkiem niewidoczne). Dedukcja typu przez decltype zwraca typ zmiennej lub wyrażenia podanego w argumencie: const int i = 0; bool f(const Widget& w); // decltype(i): const int // decltype(w): const Widget& // decltype(f): bool(const Widget&) struct Point { int x, y; // decltype(point::x): int ; // decltype(point::y): int Aby się nie dać oszukać, należy: sprawdzać nagłówki funkcji i metod (np. w źródłach bibliotek), albo.. auto cecha5 = static_cast<bool>(cechy(w)[5]); Widget w; if (f(w)) // decltype(w): Widget // decltype(f(w)): bool 56 57 Dedukcja typu przez decltype Przydaje się szczególnie kiedy szablon funkcji ma zwrócić typ danych zależny od typu parametru (ale nie będący typem parametru) funkcja zwracająca element z kontenera po uprzedniej autoryzacji dostępu do danych dla bieżącego użytkownika: template<typename Container, typename Index> (?) authandaccess(container& c, Index i) { Funkcja powinna zwracać typ taki, jak ma c[i] Jak go pozyskać? 58 Dedukcja typu przez decltype Dygresja: W C++11 wprowadzono możliwość podania typu zwracanego przez funkcję w miejscu tuż za jej nagłówkiem (tzw. trailing return type); bardzo przydatne, kiedy typ zwracanych danych ma skomplikowany zapis, np. : auto fpif(int) -> int(*)(int) Koniec dygresji. Drugie zastosowanie: kiedy zwracany typ zależy argumentów wywołania (trt): template<typename Container, typename Index> auto authandaccess(container& c, Index i) -> decltype(c[i]) { authenticateuser(); // autoryzacja dostępu return c[i]; // dostęp do danych 59 Kontrola zmiennych dynamicznych C++11 dostarcza pary szablonów klas: 1. shared_ptr 2. weak_ptr Obiekty tych typów reprezentują wskaźniki do zmiennych dynamicznych. C++ 11: Sprytne wskaźniki Ich zadanie: kontrolować zaalokowane zasoby i udostępniać tak długo, póki są potrzebne, ale nie dłużej. Automatycznie zwolnić zaalokowane zasoby, kiedy nie ma żadnego wskaźnika, który by na nie wskazywał. To pomaga zapobiegać wyciekaniu pamięci (memory leaks). 61 5
Kontrola zmiennych dynamicznych Obiekty typu shared_ptr<ty> Przechowuje wskaźnik do dynamicznie zaalokowanego zasobu typu Ty. Jeżeli wskaźnik jest różny od NULL, do zasobu mamy dostęp operatorami: operator-> oraz operator*. Obiekt typu shared_ptr<ty> przechowuje też licznik odwołań do zasobu, na który wskazuje. Kiedy kopiujemy obiekt typu shared_ptr<ty>, licznik odwołań do tego zasobu jest inkrementowany. Kiedy usuwamy kopię obiektu typu shared_ptr<ty>, licznik odwołań do tego zasobu jest dekrementowany. Kiedy licznik odwołań dla zasobu schodzi do zera, zasób jest zwalniany. 62 Kontrola zmiennych dynamicznych Obiekt typu weak_ptr<ty> Przechowuje wskaźnik do dynamicznie zaalokowanego zasobu typu Ty. Jeżeli wskaźnik jest różny od NULL, do zasobu mamy dostęp operatorami: operator-> oraz operator*. Obiekt typu weak_ptr<ty> nie przechowuje licznika odwołań do zasobu, na który wskazuje. Mimo to, też się bardzo przydaje. 63 Kontrola zmiennych dynamicznych shared_ptr posiada dynamicznie zaalokowany zasób, jeżeli: 1. Został utworzony z argumentem zawierającym wskaźnik do zasobu, 2. Został utworzony jako kopia obiektu posiadającego ten zasób, 3. Został utworzony z obiektu weak_ptr, który wskazywał na ten zasób, 4. Posiadanie zostało mu przypisane w wyniku użycia operator= lub reset. W tym przypadku przestaje posiadać poprzedni zasób, o ile taki posiadał. weak_ptr wskazuje na zasób, jeżeli: 1. Został utworzony z obiektu shared_ptr, który posiadał ten zasób, 2. Został utworzony z obiektu weak_ptr, który wskazywał na ten zasób, 3. Wskazanie zostało mu przypisane w wyniku użycia operator=. W tym przypadku przestaje wskazywać na poprzedni zasób, o ile wskazywał. 64 Kontrola zmiennych dynamicznych Obiektowi typu shared_ptr można przypisać kontrolę nad dynamicznie zaalokowanym zasobem lub uczynić pustym. Obiekt typu shared_ptr traci kontrolę nad zasobem, kiedy przypisywana mu jest kontrola nad innym zasobem, albo kiedy jest usuwany. Kiedy ostatni obiekt typu shared_ptr, posiadający pewien zasób, jest usuwany, dynamicznie zaalokowany zasób jest automatycznie zwalniany. Jeżeli się tak stanie, wszystkie obiekty typu weak_ptr, które wskazywały na ten zasób, tracą ważność (expire). 65 Deklaracja obiektu reprezentującego sprytny wskaźnik: #include <memory> struct resource { // przykładowa struktura do alokacji resource(int i0 = 0) : i(i0) { int i; ; int main() { shared_ptr<int> sp1; // konstruktor domyślny daje pusty obiekt shared_ptr<resource> sp2(new resource(3)); 66 Kontrolowany zasób ma swój usuwacz (has a deleter) jeżeli jego oryginalny właściciel otrzymał ten usuwacz w argumentach wywołania konstruktora, tj. obiekt typu shared_ptr został utworzony z dwoma argumentami konstruktora: (1) ze wskaźnikiem na zasób oraz (2) na obiekt usuwacza. struct deleter { void operator()(resource *res) { cout << "destroying resource at " << (void*)res << '\n'; delete res; ; int main() { shared_ptr<resource> sp(new resource(3), deleter()); 67 6
Kopiowanie: Uwaga: nie należy używać tego samego zasobu przy tworzeniu dwóch różnych sprytnych wskaźników, bo destruktor też będzie użyty dwa razy należy raczej: - pierwszy utworzyć j.w., - drugi za pomocą konstruktora kopiującego z tego pierwszego. Dostęp do zasobu: int *ip = new int(3); // alokowanie zasobu typu int cout << (void*)ip << '\n'; // pokazanie adresu shared_ptr<int> sp(ip); // tworzenie obiektu shared_ptr cout << (void*)sp.get() << '\n'; // pokazanie przechowywanego adresu shared_ptr<resource> sp1(new resource(4)); // sp1 przechowuje wskaźnik na zasób shared_ptr<resource> sp2(sp1); // sp2 udostępnia ten sam zasób cout << *sp << '\n'; cout << (void*)&*sp << '\n'; // pokazanie przechowywanej wartości // pokazanie adresu wartości 68 69 Dostęp do pól zasobu typu strukturalnego: struct S { int member; ; int main() { S *s = new S; // tworzenie obiektu typu S s->member = 4; // przypisanie do member shared_ptr<s> sp(s); // tworzenie obiektu shared_ptr if (sp) // konwersja na bool (przydatna!) cout << sp->member << '\n'; // pokazanie wartości member Dostęp jest identyczny, jak w przypadku zwykłych zmiennych wskaźnikowych 70 Liczenie sprytnych wskaźników odnoszących się do jednego zasobu: spi sp0; // pusty obiekt cout << "empty object: " << sp0.use_count() << '\n'; spi sp1((int*)0); // żadnego zasobu, ale niepusty cout << "null pointer: " << sp1.use_count() << '\n'; spi sp2(new int); // posiada zasób cout << sp2.unique() << '\n'; // zwraca true jedyny właściciel cout << "one object: " << sp2.use_count() << '\n'; { // tworzymy krótko żyjący obiekt sp3 spi sp3(sp2); // kopiowanie cout << "two objects: " << sp2.use_count() << '\n'; cout << sp2.unique() << '\n'; // false - dwóch właścicieli // sp3 usunięty cout << "one object: " << sp2.use_count() << '\n'; 71 Przekazywanie i zwalnianie zasobów: sps sp0(new resource(1)); // alokacja i przypisanie zasobu sps sp1(new resource(2)); // alokacja i przypisanie zasobu sps sp2(sp0); // przypisanie zasobu sp1 = sp0; // przypisanie zasobu i zwolnienie zasobu sp2.reset(); // zwolnienie zasobu sp0.swap(sp2); // zamiana przypisań zasobów if(sp0 == sp1) cout << "Nie jest dobrze." if(sp0!= sp1) cout << "Jest dobrze." Operator porównania mniejszy-niż i zastosowanie z kontenerem: #include <algorithm> #include <memory> #include <iostream> #include <set> using namespace std; typedef shared_ptr<int> spi; typedef set<spi> iset; typedef iset::const_iterator citer; static void lookup(const iset& data, spi sp) { // szuka obiektu pasującego do sp citer res = lower_bound(data.begin(), data.end(), sp); cout << *sp; if (res == data.end() *res!= sp) cout << " nie ma.\n"; else cout << " jest.\n"; ; 72 73 7
Operator porównania mniejszy-niż i zastosowanie z kontenerem: iset data; // kontener (pusty) spi sp0(new int(0)); spi sp1(new int(1)); spi sp2(new int(2)); spi sp3(sp1); // dzielenie własności z sp1 spi sp4(new int(1)); // taka sama wartość jak sp1, ale inny zasób data.insert(sp0); data.insert(sp1); data.insert(sp2); lookup(data, sp1); // szukamy sp1 - sukces lookup(data, sp3); // szukamy sp3 - sukces lookup(data, sp4); // szukamy sp4 -? Kontrola zmiennych dynamicznych weak_ptr: Obiekty tego typu przydatne są do przerywania cykli w strukturach danych. Cykl występuje wtedy, gdy dwa lub więcej kontrolowanych zasobów przechowują wskaźniki na siebie nawzajem, tak że tworzą one pętlę. Np. jeżeli dynamicznie zaalokowany zasób head zawiera pole typu shared_ptr, który wskazuje na inny zasób N1, który zawiera pole shared_ptr, wskazujące na head, to takie dwa zasoby tworzą pętlę. head shared_ptr N1 shared_ptr 74 75 Kontrola zmiennych dynamicznych weak_ptr: Ponieważ takie dwa zasoby wskazują na siebie nawzajem, żaden nie będzie miał nigdy licznika wskazań o wartości zero. Dlatego nigdy nie zostaną automatycznie usunięte, nawet, jeżeli poza nimi samymi żaden inny wskaźnik nie będzie już na nie wskazywał. Aby to zmienić N1 powinien zawierać pole typu weak_ptr, wskazujące na head. head shared_ptr N1 weak_ptr 76 Kontrola zmiennych dynamicznych weak_ptr: Jeżeli N1 zawiera pole typu weak_ptr, wskazujące na head, to: 1. Nadal można sięgnąć do head z N1. 2. Kiedy ostatni shared_ptr wskazujący na head zostanie usunięty, licznik wskazań na head będzie równy zero i head zostanie usunięty. 3. Dobrze napisany destruktor head usunie również shared_ptr wskazujący na N1 wtedy licznik wskazań na N1 będzie równy zero i N1 też zostanie usunięty. struct node { // struct do demonstracji cykli shared_ptr<node> next; weak_ptr<node> weak_next; ; 77 Kontrola zmiennych dynamicznych weak_ptr: Przykład struktury danych z cyklem i bez cyklu: node *head = new node; node *N1 = new node; shared_ptr<node> root(head); head->next = shared_ptr<node>(n1); // tworzymy pierwsze powiązanie Kontrola zmiennych dynamicznych weak_ptr: Przykład utraty ważności wskaźnika: cout << boolalpha; shared_ptr<resource> sp(new resource); weak_ptr<resource> wp(sp); cout << "wskazuje na zasób: " << wp.expired() << '\n'; cout << "liczba posiadaczy zasobu: " << wp.use_count() << '\n'; N1->next = root; N1->weak_next = root; // zapętlamy wskazania jest cykl! // zapętlamy wskazania - nie ma cyklu! sp.reset(); // ustawiamy wskazanie obiektu na NULL // wskazywany zasób dynamiczny traci jednego z // właścicieli. Tak się składa, że ostatniego (!) cout << "stracił ważność: " << wp.expired() << '\n'; 78 79 8
C++11: nullptr Zerowanie wskaźnika Do wyzerowania wskaźnika posługujemy się NULL bądź 0. To są liczby. C++ 11: nullptr Stąd może pojawić się niejednoznaczność: void fun(int); // trzy przeciążenia fun void fun(bool); void fun(void*); f(0); f(null); Dlatego w C++11 wprowadzony został nullptr. // wywoła f(int), nie f(void*) // wywoła f(int), nigdy f(void*) 81 C++11: nullptr Zerowanie wskaźnika nullptr nie jest typem całkowitoliczbowym. Działa, jakby był wskaźnikiem na każdy dowolny typ (taka magia..) f(nullptr); // wywoła f(void*) Pomaga ujednoznacznić kod, np.: auto result = findrecord( /* argumenty */ ); if (result == 0) { // result może wskaźnik, może int.. if (result == nullptr) {... Jakiego typu będzie result? 82 class Widget { ; C++11: nullptr int f1(widget* pw) { return 0; ; double f2(widget* pw) { return 0.0; ; bool f3(widget* pw) { return true; ; template<typename FuncType, typename PtrType> auto Call(FuncType func, PtrType ptr) -> decltype(func(ptr)) { return func(ptr); ; 83 C++11: nullptr auto result1 = Call(f1, 0); // 'int (Widget *)' : cannot convert parameter 1 from 'int' to 'Widget *' auto result2 = Call(f2, NULL); // 'double (Widget *)' : cannot convert parameter 1 from 'int' to 'Widget * auto result3 = Call(f3, nullptr); // działa! C++ 11: Wyrażenia lambda 84 9
Obiekty wywoływalne (callables): W C++98 predykaty logiczne i funktory arytmetyczne, przekazywane jako jeden z argumentów przy wywołaniu algorytmu, można było tworzyć na podstawie: 1. Funkcji (lub wskaźników do funkcji) o ustalonej, pasującej do potrzeb algorytmu liczbie argumentów 2. Obiektów klas wywoływalnych (z przeciążonym operatorem wywołania obiektu) W C++11 dochodzą jeszcze wyrażenia lambda: - Wywoływalny kawałek kodu (funkcja inline, bez własnej nazwy) - Mają zdefiniowany typ zwracanej wartości, argumenty wywołania i kod wykonywalny, można je definiować wewnątrz innych funkcji. 86 Wyrażenie lambda budowa: [capture list] (parameter list) -> return type { function body gdzie: capture list lista lokalnych zmiennych zadeklarowanych w otoczeniu parameter list lista parametrów wywołania return type typ zwracanych danych function body wykonywalny kod funkcji Uwaga: typ zwracanych danych jest deklarowany w miejscu tuż za nagłówkiem z wykorzystaniem strzałki -> (tj. wg notacji trailing return type); 87 Wyrażenie lambda budowa: [capture list] (parameter list) -> return type { function body Można pominąć listę parametrów oraz informację o typie zwracanych danych, ale muszą wystąpić: lista lokalnych zmiennych oraz kod wykonywalny, np.: auto f = [] { return 42; ; f obiekt wywoływalny, który nie przyjmuje żadnych argumentów i zwraca 42. Wywołanie jest identyczne jak w przypadku funkcji: cout << f() << endl; // wypisuje w oknie konsoli 42 Pominięcie informacji o typie zwracanych danych: typ zwracany to void, lub jeżeli kod zawiera tylko return, to typ jest dedukowany na tej podstawie. 88 Wyrażenie lambda z argumentami: porównanie długości dwóch napisów [](const string &a, const string &b) { return a.size() < b.size(); Argumentom nie można przypisywać wartości domyślnych. Przykład wykorzystania wywołanie algorytmu sortującego słowa w kontenerze words, z wykorzystaniem predykatu zdefiniowanego w postaci lambdy : stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size();); 89 Przechwytywane zmienne lista: [ ] lista przechwyconych wartości jest pusta, kod wyrażenia lambda używa tylko tego, co zostało przekazane w argumentach wywołania [nazwa1, &nazwa2,..] lista zmiennych lokalnych przechwyconych domyślnie przez wartość; jeżeli przed nazwą stoi symbol &, to następuje przechwycenie przez referencję Wyrażenie lambda z listą lokalnych zmiennych przechwyconych przez wartość: sprawdzenie, czy długość jest większa od wartości przechowywanej w zmiennej sz: [sz](const string &a) { return a.size() >= sz; ; W kodzie wyrażenia lambda można odwoływać się tylko do tych zmiennych dostępnych lokalnie w otoczeniu wyrażenia, które zostały zadeklarowane w liście lokalnych zmiennych. 90 Przykład wykorzystania znalezienie w kontenerze words pierwszego słowa o długości równej lub większej niż sz: auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; ); 91 10
Wyrażenie lambda i zmienne globalne: Po znalezieniu pierwszego słowa o odpowiedniej długości, możemy następnie dodać kod, który wypisuje wszystkie słowa występujące po znalezionym słowie: for_each(wc, words.end(), [](const string &s){cout << s << " ";); cout << endl; Uwaga: mimo, że lista zmiennych lokalnych jest pusta, kod korzysta z cout (?) 1. Lista lokalnych zmiennych służy do przekazania do kodu wyrażenia wyłącznie zmiennych lokalnych non-static. 2. Zmienne zdefiniowane poza funkcją, gdzie wywołana została lambda, ale dostępne wewnątrz tej funkcji, są również dostępne w kodzie lambda. Obiekt lambda: 1. Kiedy przekazujemy wyrażenie lambda do funkcji, tworzymy nowa klasę oraz obiekt tej klasy, którym będzie argument wywołania tej funkcji. 2. Odpowiednio, kiedy używamy auto, żeby zdefiniować zmienną zainicjalizowaną wyrażeniem lambda, zmienna ta jest obiektem nowo utworzonej klasy. 3. Klasa utworzona z wyrażenia lambda posiada listę pól odpowiadających przechwyconym zmiennym; są one inicjalizowane, kiedy obiekt jest tworzony. 92 93 Obiekt lambda: auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; ); wygeneruje klasę, która będzie wyglądała tak, jak klasa SizeComp poniżej: class SizeComp { SizeComp(size_t n): sz(n) { // każdy kolejny parametr // konstruktora odpowiada jednej przechwytywanej zmiennej; // operator wywołania, który zwraca daną takiego samego // typu, ma takie same parametry i kod jak wyrażenie lambda bool operator()(const string &a) const { return a.size() >= sz; private: size_t sz; // pola odpowiadają przechwytywanym zmiennym Przechwytywanie zmiennych: Zmienne lokalne przechwycone przez wyrażenie mogą zostać przechwycone w trybie przez referencję bądź przez wartość. W przykładach dotychczas przez wartość: void fcn1() { size_t v1 = 42; // zmienna lokalna auto f = [v1]{ return v1;; // kopiowanie v1 do obiektu v1 = 0; auto j = f(); // j==42 ponieważ f przechowuje kopię v1 ; 94 95 Zmienne przechwycone przez referencję: void fcn2() { size_t v1 = 42; // zmienna lokalna // obiekt f2 przechowuje referencję do v1 auto f2 = [&v1] { return v1; ; v1 = 0; auto j = f2(); // j==0 ponieważ f2 ma referencję do v1 Uwaga: kiedy przechwytujemy obiekt przez referencję, musimy być pewni, że będzie nadal istniał w momencie działania kodu wyrażenia lambda 96 Zmienne przechwycone przez referencję: Przykład, kiedy przechwycenie przez referencję jest konieczne: void funwords(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') { // tutaj kod, który np. porządkuje słowa // wg określonego kryterium // a teraz kod który je wypisuje do strumienia // podanego w argumencie wywołania funkcji 'funwords' for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; ); 97 11
Zmienne przechwycone przez referencję - podsumowanie: Zmienne typów bazowych int, double najprościej przechwytywać w obiekcie lambda w trybie przez wartość. Kiedy przechwytujemy w trybie przez referencję, np. iteratory, lub wskaźniki, należy zapewnić istnienie wskazywanych struktur danych, kiedy obiekt lambda będzie wykonywany, oraz zapewnić, że zawartość będzie taka, jakiej oczekiwaliśmy. Funkcja, w której powstał obiekt lambda, może go na zakończenie zwrócić, ale zmienne lokalne funkcji nie mogą być w nim przechowane przez referencję. Zmienne przechwycone implicite: [=] przechwycenie przez wartość wszystkich zmiennych lokalnych z otoczenia [&] przechwycenie przez referencję wszystkich zmiennych lokalnych z otoczenia [=, lista referencji] przechwycenie przez referencję zmiennych wymienionych w liście referencji, natomiast przez wartość wszystkich pozostałych zmiennych lokalnych z otoczenia [&, lista zmiennych] przechwycenie przez wartość zmiennych wymienionych w liście zmiennych, natomiast przez referencję wszystkich pozostałych zmiennych lokalnych z otoczenia 98 99 Zmienne przechwycone implicite: sprawdzenie, czy długość jest większa od wartości przechowywanej w zmiennej sz: wc = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() >= sz; ); 100 Zmienne przechwycone implicite i explicite: Przykład na mieszane przechwytywanie implicite i explicite (domyślne i jawne): char c = ' '; for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c; ); Dla mieszanego sposobu przechwytywania pierwszym elementem w liście przechwytywanych zmiennych musi być & lub =. Ten symbol ustala domyślny sposób przechwytywania, który będzie użyty wobec zmiennych niewymienionych po przecinku. Zmienne wymienione po przecinku musza być przechwytywane w sposób przeciwny do domyślnego. 101 Modyfikacja przechwyconych zmiennych: Domyślnie w kodzie lambdy nie wolno jest modyfikować zmiennych przechwyconych przez wartość. Jeżeli potrzebna jest modyfikacja, stosujemy słowo kluczowe mutable size_t v1 = 42; // v1 - zmienna lokalna auto f3 = [v1] () mutable { return ++v1; ; v1 = 0; auto j = f3(); // j == 43 Modyfikacja przechwyconych zmiennych: O tym, czy zmienna przechwycona sposobem przez referencje może być zmieniana, decyduje wyłącznie to, czy referencja dotyczy zmiennej typu const, czy nie. size_t v1 = 42; // zmienna lokalna, która nie jest const // 'v1' jest referencją na zmienną lokalną nie-const auto f2 = [&v1] { return ++v1; ; // modyfikuje 'v1' v1 = 0; auto j = f2(); // j == 1 102 103 12
Specyfikowanie typu zwracanych danych: Kiedy w kodzie wyrażenia lambda występuje tylko instrukcja return nie ma potrzeby specyfikowania typu zwracanych danych, np. : transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i < 0? -i : i; ); Kiedy w kodzie wyrażenia występuje jakiekolwiek inne wyrażenie niż return, domyślnie wyrażenie lambda zwraca void. transform(vi.begin(), vi.end(), vi.begin(), [](int i) { if (i<0) return -i; else return i; ); Specyfikowanie typu zwracanych danych: Aby jawnie podać zwracany typ danych należy typ zwracanych danych zadeklarować w miejscu tuż za nagłówkiem z wykorzystaniem strzałki -> (tj. wg notacji trailing return type); transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; ); Pojawia się problem, bo miała być zwrócona wartość absolutna, a nie ma nic.. 104 105 13