Täiendavad teemad
Pärast selle praktikumi läbimist üliõpilane
- oskab luua lambda avaldisi ja funktsioone (lambda-function)
- oskab lambda funktsioone rakendada töös andmekogumitega
- teab mõningaid STL algoritme ja oskab neid koos lambda-funktsioonidega kasutada
- oskab luua koopia- ja teisalduskonstruktorit ja teab, kus neid kasutada
Lambda avaldis (funktsioon)
Lambda-funktsioonid on mehhanism anonüümsete funktsioonide koostamiseks. Lambda-funktsiooni signatuur on järgmine (paralleelselt lisatud ka ingliskeelse tekstiga):
[kaasatud muutujate loetelu](parameetrid)->tagastustüüp{//funktsiooni keha}; [capture list](parameters)->return_type{//function body};
Näide lihtsast lambda-funktsioonist, kus ei ole parameetreid ega ka kaasatud muutujate loetelu. Paneme tähele, et funktsiooni keha lõpetava loogelise sulu järel on semikoolon.
#include <iostream> using namespace std; int main() { [](){ cout << "Tere, maailm!" << '\n'; }; return 0; } | Tere, maailm! |
Lambda-funktsioonile saab ette anda parameetreid ja nende väärtusi (argumente). Järgmises näites anname lambda-funktsioonile ette kaks parameetrit:
[](float a, float b){ //kaks parameetrit cout << "a + b = " << a + b << '\n'; }(2.5, 4.3); // argumendid | a + b = 6.8 |
Lambda funktsioonile võib anda nime ja pöörduda tema poole nime järgi. Samuti võib ta andmeid tagastada ja olla avaldise osa.
auto fun = [](float a, float b)->float{ // võib ka float fun return (a + b); }(2.5, 4.3); cout << fun << '\n'; // lambda-funktsiooni poole pöördumine cout << [](float a, float b)->float{return (a + b);}(2.5, 4.3) << '\n'; // lambda-funktsioon avaldises auto tul = 3.2 + [](float a, float b)->float{return (a + b);}(2.5, 4.3); // lambda-funktsioon avaldises auto tul1 = 3.2 + fun; cout << tul << " " << tul1 << '\n'; | 6.8 6.8 10 10 |
Kui defineerime argumentideta lambda-funktsiooni, siis pöördumisel tuleb argumendid ette anda:
auto fun = [](float a, float b)->float{ return (a + b); }; // argumente ei ole cout << fun(2.5, 4.3) << '\n'; // argumendid pöördumisel auto tul1 = 3.2 + fun(2.5, 4.3); // argumendid pöördumisel cout << tul1 << '\n'; | 6.8 10 |
Muutujaid saab kaasata (capture) nii väärtuse kui ka viite (&) abil. Järgmistes näidetes jätame lihtsuse mõttes parameetrid ära. Muutujate kaasamisel väärtuse järgi kopeeritakse väärtus lambda funktsiooni loomisel ja edaspidi kasutatakse ainult seda väärtust:
int c = 10; auto fun = [c](){ // kaasatakse muutuja c väärtus (kopeeritakse) cout << "Funktsiooni sees " << c << '\n'; }; for (int i = 0; i < 3; ++i) { cout << "Väljaspool funktsiooni " << c << '\n'; ++c; fun(); // lambda-funktsiooni poole pöördumine } | Väljaspool funktsiooni 10 Funktsiooni sees 10 Väljaspool funktsiooni 11 Funktsiooni sees 10 Väljaspool funktsiooni 12 Funktsiooni sees 10 |
Kui kaasame muutuja viite (&) abil, siis muutuja väärtuse muutmine programmis mõjub ka lambda-funktsioonis.
int c = 10; auto fun = [&c](){ // kaasatakse muutuja c väärtus viitena cout << "Funktsiooni sees " << c << '\n'; }; for (int i = 0; i < 3; ++i) { cout << "Väljaspool funktsiooni " << c << '\n'; ++c; fun(); } | Väljaspool funktsiooni 10 Funktsiooni sees 11 Väljaspool funktsiooni 11 Funktsiooni sees 12 Väljaspool funktsiooni 12 Funktsiooni sees 13 |
STL konteineri algoritmid
Lambda-funktsioone on mugav kasutada nii massiivide kui ka STL konteinerite korral. STL konteineri moodulid <algorithm>
ja <numeric>
sisaldavad üle 100 erineva algoritmi, mis lihtsustavad tööd andmekogumitega. Siinkohal vaatleme neist mõnda. Vaatame näidet algoritmiga all_of
. Esimesed kaks parameetrit määravad andmekogumil algus- ja lõpuiteraatori (st elemendid, mille peale algoritmi rakendada); kolmas parameeter on predikaat, mida rakendatakse kõikide eelnevalt määratud elementide peal. Algoritm all_of
tagastab true
, kui predikaat tagastab true
kõikide kontrollitud elementide korral. Predikaadiks on antud juhul lambda-funktsioon, mis tagastab true
, kui arv on paarisarv.
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v1{2, 8, 12, 6, 14}; if (all_of(v1.begin(), v1.end(), [](int i){return i%2 == 0;})){ // kolmas argument on lambda-funktsioon cout << "Vektoris v1 on kõik paarisarvud\n"; } else{ cout << "Vektoris v1 on ka paarituid arve\n"; } return 0; } | Vektoris v1 on kõik paarisarvud |
Märgime siinjuures, et predikaadiks sobib ka tavaline funktsioon, mis tagastab sobiva tõeväärtuse:
#include <iostream> #include <vector> #include <algorithm> using namespace std; bool kas_paaris(int n){ return (n % 2 == 0); } int main() { vector<int> v2{2, 8, 12, 6, 14, 1}; if (all_of(v2.begin(), v2.end(), kas_paaris)){ // predikaadiks on funktsioon kas_paaris cout << "Vektoris v2 on kõik paarisarvud\n"; } else{ cout << "Vektoris v2 on ka paarituid arve\n"; } return 0; } | Vektoris v2 on ka paarituid arve |
Algoritm for_each
rakendab kolmanda parameetrina antud funktsiooni kõigil elementidel, mis on määratud kahe esimese parameetriga:
#include <iostream> #include <set> #include <algorithm> using namespace std; int main() { set<int> s1{2, 53, 41, 334}; int sum{0}; for_each(s1.begin(), s1.end(), [&sum](int n){sum += n;}); // sum antakse edasi viitena cout << "Elementide summa = " << sum << '\n'; return 0; } | Elementide summa = 430 |
Lambda-funktsioonil on üks parameeter, milleks on täisarv. Funktsiooni täidetakse hulga kõikide elementide korral.
Andmekogumite korral läheb tihti vaja sortimist. Esimese näitena vaatame vektori elementide sortimist. Täisarvud on võrreldavad, seega pole vaja võrdlemiseeskirja ette anda.
vector<int> v{12, 8, 12, 6, 14, 1, 3, 4}; sort(v.begin(), v.end()); // STL sort algoritm for_each(v.begin(), v.end(), [](int n){cout << n << " ";}); // for_each elementide kuvamiseks ekraanile | 1 3 4 6 8 12 12 14 |
Teise näitena vaatame objektide sortimist algoritmi sort
abil. Kompaktsuse huvides on kogu kood ühes failis. Klassis Raamat
on väljad aasta ja pealkirja jaoks. Raamatuid võrreldakse aasta järgi.
#include <iostream> #include <ostream> #include <vector> #include <algorithm> #include <string> using namespace std; class Raamat{ int m_aasta{}; string m_pealkiri{}; public: Raamat() = default; Raamat(int aasta, string pealkiri) : m_aasta{aasta}, m_pealkiri{pealkiri}{} int getAasta(){ return m_aasta; } friend ostream& operator<<(ostream& os, Raamat r){ os << r.m_pealkiri << " " << r.m_aasta; return os; } }; int main() { // Raamatute vektor vector<Raamat> raamatud {Raamat{2021, "Füüsika õhtuõpik"}, Raamat{2022, "Mikside raamat"}, Raamat{2015, "Teatrielu"}}; // lambda-funktsioon raamatute võrdlemiseks auto vrdl = [](Raamat r1, Raamat r2){ return (r1.getAasta() < r2.getAasta()); }; // raamatute kuvamine for_each abil, kasutab operaatorit << for_each(raamatud.begin(), raamatud.end(), [](Raamat r){cout << r << '\n';}); // raamatute sortimine lambda-funktsiooni vrdl abil sort(raamatud.begin(), raamatud.end(), vrdl); cout << "=========\n"; // raamatute kuvamine for_each abil for_each(raamatud.begin(), raamatud.end(), [](Raamat r){cout << r << '\n';}); return 0; } | Füüsika õhtuõpik 2021 Mikside raamat 2022 Teatrielu 2015 ========= Teatrielu 2015 Füüsika õhtuõpik 2021 Mikside raamat 2022 |
Täpsemalt saab lambda-avaldist ja funktsioonide kohta uurida aadressil
https://en.cppreference.com/w/cpp/language/lambda
STL algoritmide kohta saab uurida aadressil
https://en.cppreference.com/w/cpp/algorithm
Kopeerimisest ja teisaldamisest
Loome lisaks klassi Isik
, milles on üks väli nime jaoks. Täiendame klassi Raamat
veel ühe väljaga, milleks on autor, st viit (pointer) isikule. Funktsioonis main
loome ühe raamatu. Teise raamatu loome käsuga Raamat r2{r1}
, kus toimub pöördumine C++ kompilaatori poolt tehtud koopiakonstruktori poole. Kuvame ekraanile mõlema objekti aadressi ja mõlema autori aadressi.
#include <iostream> #include <ostream> #include <string> using namespace std; class Isik{ string m_nimi{}; public: Isik() = default; Isik(string nimi) : m_nimi{nimi}{} string getNimi(){ return m_nimi; } string to_String(){ return m_nimi; } }; class Raamat{ int m_aasta{}; string m_pealkiri{}; Isik* m_autor{}; public: Raamat() = default; Raamat(int aasta, string pealkiri, Isik* autor) : m_aasta{aasta}, m_pealkiri{pealkiri}, m_autor{autor}{} int getAasta(){ return m_aasta; } string getPealkiri(){ return m_pealkiri; } Isik* getAutor(){ return m_autor; } friend ostream& operator<<(ostream& os, Raamat r){ os << r.m_pealkiri << " " << r.m_aasta << " " << r.getAutor()->to_String(); return os; } }; int main() { Isik* autor1 = new Isik{"Jüri Liiv"}; Raamat r1{2021, "Füüsika õhtuõpik", autor1}; Raamat r2{r1}; //koopiakonstruktori poole pöördumine cout << "r1 aadress: " << &r1 << " " << r1 << " " << r1.getAutor() << '\n'; cout << "r2 aadress: " << &r2 << " " << r2 << " " << r2.getAutor() << '\n'; delete autor1; return 0; } | r1 aadress: 0xee8bff860 Füüsika õhtuõpik 2021 Jüri Liiv 0x1b8dd6618f0 r2 aadress: 0xee8bff830 Füüsika õhtuõpik 2021 Jüri Liiv 0x1b8dd6618f0 |
Näeme, et raamatute aadressid on erinevad, aga viit autorile on sama. See tähendab, et vaikimisi koopiakonstruktor teeb objektist lameda (madala) (shallow) koopia. Lameda koopia korral kopeeritakse objekt bait-baidilt, sealhulgas ka viidad. Seega mõlemad koopiad viitavad samale Isik
objektile. Selleks, et teha täielikku (sügavat) koopiat (deep copy), tuleb üle defineerida koopiakonstruktor. Koopiakonstruktorile tuleb objekt ette anda viitena (&
). Lisame ka ekraanile kuvamise, et täidetakse koopiakonstruktorit.
Raamat(Raamat& r) : m_aasta{r.getAasta()}, m_pealkiri{r.getPealkiri()}, m_autor{new Isik(*(r.getAutor()))} { cout << "Koopiakonstruktoris " << m_pealkiri << '\n'; }
Nüüd on programmi väljundiks
Koopiakonstruktoris Füüsika õhtuõpik r1 aadress: 0x6d03bff680 0x17109c518f0 r2 aadress: 0x6d03bff650 0x17109c51a90
Näeme, et ka autorist on tehtud koopia, st et on tegemist täieliku koopiaga (deep copy).
Koopiakonstruktor kutsutakse välja enamasti järgmistel juhtudel
- kui objekt tagastatakse väärtusena
- kui objekt edastatakse (funktsioonile) väärtusena
- kui objekt luuakse teise objekti pealt
Koopiakonstruktori korral tehakse objektist koopia ja vana objekt jääb alles. Teisalduskonstruktori korral tehakse uus objekt ja vana objekt muudetakse kehtetuks.
Raamat(Raamat&& r); //teisalduskonstruktor
Teisalduskonstruktori parameetriks on siin Raamat&&
, kus &&
tähistab rvalue reference
. Mõiste rvalue
on täiendiks mõistele lvalue
, mis laias laastus tähendab "miski, mis saab olla omistamise vasakul poolel". Ehk siis (esimeses lähenduses), rvalue
on miski, mida ei saa muutujale omistada ja me saame "kaaperdada" tema väärtuse. Täidame konstruktori sisuga:
// teisalduskonstruktor Raamat(Raamat&& r) : m_aasta{r.m_aasta}, m_pealkiri{r.m_pealkiri}, m_autor{r.m_autor}{ r.m_aasta = 0; r.m_pealkiri = ""; r.m_autor = nullptr; cout << "Teisalduskonstruktoris " << m_pealkiri << '\n'; }
Teisalduskonstruktorit (kui see on defineeritud) kasutab vector
elementide lisamisel.
int main() { vector<Raamat> raamatud{}; Isik *autor1 = new Isik{"Jüri Liiv"}; raamatud.push_back(Raamat{2021, "Füüsika õhtuõpik", autor1}); cout << "\n"; for (Raamat &r: raamatud) { cout << r << '\n'; } delete autor1; return 0; } | Teisalduskonstruktoris Füüsika õhtuõpik Füüsika õhtuõpik 2021 Jüri Liiv |
Kui lisame vektorisse mitu raamatut, siis mõnikord pöördutakse teisalduskonstruktori poole ka juba lisatud raamatute korral. Põhjus on selles, et vektori elementide jaoks võetakse ruumi juurde tükkide kaupa. Vektori mahutavuse saame funksiooni capacity
abil. Kui elemendi lisamisel vektorisse on mahtuvus täis, siis võetakse ruumi juurde ja elemendid tõstetakse ringi. Järgmises näites kuvatakse enne elemendi lisamist vektorisse vektori mahutavus.
int main() { vector<Raamat> raamatud{}; Isik *autor1 = new Isik{"Jüri Liiv"}; Isik *autor2 = new Isik{"J.K. Rowling"}; cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2021, "Füüsika õhtuõpik", autor1}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{1997, "Harry Potter ja tarkade kivi", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{1998, "Harry Potter ja saladuste kamber", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{1999, "Harry Potter ja Azbakani vang", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2000, "Harry Potter ja tulepeeker", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2003, "Harry Potter ja Fööniksi ordu", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2005, "Harry Potter ja segavereline prints", autor2}); cout << "\n"; for (Raamat &r: raamatud) { cout << r << '\n'; } delete autor1; delete autor2; return 0; } | raamatud.capacity() = 0 Teisalduskonstruktoris Füüsika õhtuõpik raamatud.capacity() = 1 Teisalduskonstruktoris Harry Potter ja tarkade kivi Teisalduskonstruktoris Füüsika õhtuõpik raamatud.capacity() = 2 Teisalduskonstruktoris Harry Potter ja saladuste kamber Teisalduskonstruktoris Füüsika õhtuõpik Teisalduskonstruktoris Harry Potter ja tarkade kivi raamatud.capacity() = 4 Teisalduskonstruktoris Harry Potter ja Azbakani vang raamatud.capacity() = 4 Teisalduskonstruktoris Harry Potter ja tulepeeker Teisalduskonstruktoris Füüsika õhtuõpik Teisalduskonstruktoris Harry Potter ja tarkade kivi Teisalduskonstruktoris Harry Potter ja saladuste kamber Teisalduskonstruktoris Harry Potter ja Azbakani vang raamatud.capacity() = 8 Teisalduskonstruktoris Harry Potter ja Fööniksi ordu raamatud.capacity() = 8 Teisalduskonstruktoris Harry Potter ja segavereline prints Füüsika õhtuõpik 2021 Jüri Liiv Harry Potter ja tarkade kivi 1997 J.K. Rowling Harry Potter ja saladuste kamber 1998 J.K. Rowling Harry Potter ja Azbakani vang 1999 J.K. Rowling Harry Potter ja tulepeeker 2000 J.K. Rowling Harry Potter ja Fööniksi ordu 2003 J.K. Rowling Harry Potter ja segavereline prints 2005 J.K. Rowling |
Lõpetuseks
Olemegi jõudnud kursuse lõppu. Kahjuks jäid seekord kursusel tähelepanuta (või väga põgusa tähelepanuga) mitmed olulised teemad. Püüa nendega vajadusel ise tutvust teha. Nimetame siin mõnda:
- nimeruumid (namespaces)
https://en.cppreference.com/w/cpp/language/namespace
- moodulid (modules)
https://en.cppreference.com/w/cpp/language/modules
- lõimed (threads)
https://en.cppreference.com/w/cpp/thread/thread/thread
- ranges
https://en.cppreference.com/w/cpp/ranges
- coroutines
https://en.cppreference.com/w/cpp/language/coroutines
- ...
Mõnusat programmeerimist keeles C++!