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{
[capture list](parameters)->return_type{
|
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 ;
}
|
| |
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){
cout << "a + b = " << a + b << '\n' ;
}( 2.5 , 4.3 );
|
| |
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 {
return (a + b);
}( 2.5 , 4.3 );
cout << fun << '\n' ;
cout << []( float a, float b)-> float { return (a + b);}( 2.5 , 4.3 ) << '\n' ;
auto tul = 3.2 + []( float a, float b)-> float { return (a + b);}( 2.5 , 4.3 );
auto tul1 = 3.2 + fun;
cout << tul << " " << tul1 << '\n' ;
|
| |
Kui defineerime argumentideta lambda-funktsiooni, siis pöördumisel tuleb argumendid ette anda:
auto fun = []( float a, float b)-> float {
return (a + b);
};
cout << fun( 2.5 , 4.3 ) << '\n' ;
auto tul1 = 3.2 + fun( 2.5 , 4.3 );
cout << tul1 << '\n' ;
|
| |
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](){
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 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](){
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 ;})){
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)){
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;});
cout << "Elementide summa = " << sum << '\n' ;
return 0 ;
}
|
| |
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());
for_each(v.begin(), v.end(), []( int n){cout << n << " " ;});
|
| |
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() {
vector<Raamat> raamatud {Raamat{ 2021 , "Füüsika õhtuõpik" },
Raamat{ 2022 , "Mikside raamat" },
Raamat{ 2015 , "Teatrielu" }};
auto vrdl = [](Raamat r1, Raamat r2){
return (r1.getAasta() < r2.getAasta());
};
for_each(raamatud.begin(), raamatud.end(), [](Raamat r){cout << r << '\n' ;});
sort(raamatud.begin(), raamatud.end(), vrdl);
cout << "=========\n" ;
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};
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.
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:
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:
https://en.cppreference.com/w/cpp/language/namespace
https://en.cppreference.com/w/cpp/language/modules
https://en.cppreference.com/w/cpp/thread/thread/thread
https://en.cppreference.com/w/cpp/ranges
https://en.cppreference.com/w/cpp/language/coroutines
Mõnusat programmeerimist keeles C++!