Detyrë: Të shkruhet klasa Studenti
me fushat private emri
, mbiemri
, dhe notat[5]
.
notaMesatare
dhe emriPlote
.main
të deklarohen dy studentë dhe të ruhet në një variabël studenti me notën mesatare më të madhe.Kur mund të themi se dy objekte janë të njëjta?
Nuk ka përgjigje definitive për këtë pyetje – varet nga konteksti i përdorimit.
Secila klasë në C++ ka një konstruktor të kopjimit.
Nëse nuk mbishkruhet, nënkuptohet si kopjim i të gjitha fushave përmes vlerës.
Kopja përmes vlerës quhet kopje e cekët, pasi që nuk kopjon objektet e referencuara nga pointerët.
Kopja e thellë kopjon edhe objektet e referencuara.
Shpesh do të dizajnojmë tipe të cilat nuk kemi dëshirë të kopjohen në mënyrë implicite.
Për këtë arsye e ndalojmë operatorin =
dhe konstruktorin T(T&)
me delete
:
class Studenti {
private:
// Konstruktori i kopjimit.
Studenti(const Studenti&) = delete;
// Operatori i shoqërimit.
Studenti& operator=(const Studenti&) = delete;
};
Tipet që nuk mund të kopjohen mund të bartën vetëm përmes referencës. Pse?
Semantikat e lëvizjes
Lëvizja e objektit nënkupton bartjen e pronësisë së resurseve që i menaxhon te një objekt tjetër.
Kjo është më efikase sesa kopjimi i thellë.
Objekti burimor “humb” pronësinë, duke ia pamundësuar lirimin e resurseve në destruktor.
Në C++ 11 është shtuar një tip i ri i referencës – referencat në R-Values.
Shënohet ngjashëm me referencat e zakonshme:
void funksioni(Tipi&& rval) {
...
}
Move constructori mundëson bartjen e vlerave nga një R-Value të përkohshme në objektin e anës së majtë.
class Studenti {
public:
// Konstruktori i lëvizjes.
Studenti(Studenti&& tjeter) { ... }
};
Kjo na nevojitet për të shmangur kopjimin e tipeve referente të alokuara statikisht.
Pra, për një tip referent i vendosim këto kushtëzime:
Detyrë: Të shkruhet klasa Varg
me anëtarët:
data
e tipit int*
dhe n
e tipit int
.Varg(*data, n)
i cili inicializon fushat përmes kopjimit.~Varg()
i cili liron memorien data
.at(i)
i cili kthen elementin në pozitën i
.length()
i cili kthen gjatësinë e vargut.Funksionet na kanë mundësuar ta gjeneralizojmë logjikën për çfarëdo vlere të parametrit x
.
Ndonjëherë mund ta gjeneralizojmë një logjikë ose strukturë për çfarëdo tipi T
.
Funksioni që mund të aplikohet mbi tipe të ndryshme quhet funksion gjenerik (ang. generic).
Struktura e të dhënave që mund të mbajë tipe të ndryshme quhet tip i të dhënave gjenerik.
Në C++ gjeneralizimi i funksioneve dhe tipeve arrihet përmes templates (shablloneve).
Sintaksa:
template<typename T1, typename T2, typename TRez, ...>
TRez funksioni(T1 arg1, T2 arg2, ...) {
...
}
template<typename T>
struct Tipi {
T fusha;
};
Shembull: Struktura Varg<T>
mban një varg dhe gjatësinë e saj për çfarëdo tipi T
:
template<typename T>
struct Varg {
T* data;
int n;
};
int main() {
int ptr1[4] = { 1, 2, 3, 4 };
char ptr2[3] = { 'a', 'b', 'c' };
Varg<int> v1 = {ptr1, 4};
Varg<char> v2 = {ptr2, 3};
return 0;
}
Shumë tipe të ndryshme të të dhënave ndajnë natyrë të njëjtë të sjelljes. Funksionet e gjeneralizuara e nxjerrin të përbashkët këtë logjikë.
Në vend se të kemi shumë mbingarkime, ndonjëherë mund ta kemi një funksion të vetëm.
Detyrë: Të shkruhet funksioni swap(T& a, T& b)
i cili ndërron vendet e dy variablave të tipit T
.
Detyrë: Të gjeneralizohet tipi Varg
ashtu që të mbajë çfarëdo tipi T
, pra Varg<T>
.
Shpesh kompajlleri kupton llojin e variablës prej vlerës së inicializuar nga ana e djathtë.
Në këto raste variablën mund ta deklarojmë si auto.
auto x = 3; // int
auto y = 1.5; // double
auto z = "pershendetje"; // const char*
Detyrë: Të tregohen tipet e variablave në vijim.
auto a = 5;
auto b = a / 2.0;
auto c = (b > 3.0);
auto d = a % 2;
auto e = (c ? 'x' : 'y');
Kur një funksion përdoret si shprehje atëherë njihet si funksion anonim ose lambda shprehje.
Këto shprehje nuk janë doemos të lidhura me ndonjë identifikator.
Funksioni si shprehje ka tipin function<U(T)>
.
Ky funksion merr një argument T
dhe kthen një vlerë U
.
Ky tip gjendet në headerin <functional>
.
Sintaksa për lambda shprehje:
[konteksti](tipi1 arg1, tipi2 arg2, ...) -> tipi_kthyes {
...
return ...;
}
Kur tipi kthyes është i nënkuptuar nga kompajlleri mund të mos shkruhet fare.
Shembull: Lambda që teston a është argumenti çift.
[](int x) -> bool { return x % 2 == 0; }
Ose shkurtimisht:
[](int x) { return x % 2 == 0; }
Kur dëshirojmë ta përdorim një lambda zakonisht e lidhim për ndonjë variabël:
function<bool(int)> eshte_cift = [](int x) -> bool {
return x % 2 == 0;
};
Ose shkurtimisht:
auto eshte_cift = [](int x) { return x % 2 == 0; };
Zakonisht jemi mësuar që variablat të mbajnë shënime. Në këto shembuj variablat po mbajnë funksione.
Lambdat ruhen si instanca të tipeve speciale të quajtura function object ose functor.
Shembull: Thirrja e funksionit anonim.
#include <iostream>
using namespace std;
int main() {
auto eshte_cift = [](int x) { return x % 2 == 0; };
if (eshte_cift(2)) {
cout << "Numer cift." << endl;
} else {
cout << "Numer tek." << endl;
}
return 0;
}
Detyrë: Të tregohet dalja në ekran për kodin në vazhdim.
auto funks1 = [](int x) { return x + 2; };
auto funks2 = [](int x) { return x - 1; };
auto f = (2 > 3) ? (funks1) : (funks2);
cout << f(4);
Detyrë: Të deklarohen 3 lambda për veprimet:
Në main
të lidhen këto vlera për variabla lokale dhe të thirren me ndonjë argument sipas dëshirës.
Pasi që lambdat janë shprehje, ato mund të përdoren si parametra dhe vlera kthyese të funksioneve.
Funksionet që pranojnë ose kthejnë funksione tjera njihen si funksione të rendit të lartë.
Në të ardhmën do të mësojmë funksione të shpeshta si map
, filter
, reduce
, any
, all
, etj.
Shembull: Funksioni që teston a vlen një predikat për një argument të dhënë.
void testo(int vlera, function<bool(int)> predikati) {
if (predikati(vlera)) {
cout << "Kushti vlen." << endl;
} else {
cout << "Kushti nuk vlen" << endl;
}
}
...
testo(7, [](int x) { return x > 5; });
testo(2, [](int x) { return x > 5; });
testo(3, [](int x) { return x % 2 == 0; });
Detyrë: Të shkruhet funksioni ekziston(v,n,f)
, i cili pranon një varg të numrave
dhe kthen true
nëse së paku ndonjëri prej tyre përmbush kushtin f
(predikatin).
Konteksti i funksionit anonim
Trupi i lambdës mund të referencojë vlera nga blloku i deklarimit të saj.
Kapja mund të bëhet përmes vlerës ose përmes referencës.
Instanca e funksionit bashkë me rrethinën e saj (vlerat e kapura) njihet si closure.
Kapja përmes vlerës dhe referencës:
int shtesa = 2;
auto zmadho_val = [shtesa](int x) { return x + shtesa; };
auto zmadho_ref = [&shtesa](int x) { return x + shtesa; };
shtesa++;
cout << zmadho_val(5) << endl;
cout << zmadho_ref(5) << endl;
Ekzistojnë dy sintaksa shkurtesë:
[=]
i kap automatikisht vlerat e përdorura përmes vlerës.[&]
i kap automatikisht vlerat e përdorura përmes referencës.Në shumë gjuhë lambdat shënohen me sintaksen shigjetë: $x => \text{trupi}(x)$.
Funksionin katrori mund ta interpretojmë si transformimin: $x \Rightarrow x^2$.
Në C++, sintaksën me shigjetë mund ta imitojmë përmes #define
:
#define lambda(arg, expr) ([](auto arg) { return (expr); })
lambda(x, x * x) // x => x * x
Tipin function<U(T)>
mund ta shkruajmë edhe si:
template<typename T, typename U>
using func = function<U(T)>;
Psh. tipi func<int,bool>
paraqet funksionin që merr një int
dhe kthen një bool
.
func<int, bool> eshte_cift = lambda(x, x % 2 == 0);
Detyrë: Të shkruhet funksioni i rendit të lartë shuma(n,f)
i cili llogarit shumën $\sum{f(i)}$ për $i\in 1\dots n$.
–
#include <iostream>
#include <functional>
#define lambda(arg, expr) ([](auto arg) { return (expr); })
using namespace std;
template<typename T, typename U>
using func = function<U(T)>;
int shuma(int n, func<int, int> f) {
int s = 0;
for (int i = 1; i <= n; i++) {
s += f(i);
}
return s;
}
int main() {
int n;
cout << "Shtyp n: ";
cin >> n;
// Sintaksa e zakonshme
cout << shuma(n, [](int i) { return 3 * i + 2; }) << endl;
cout << shuma(n, [](int x) { return x * x; }) << endl;
// Macro
cout << shuma(n, lambda(i, 3 * i + 2)) << endl;
cout << shuma(n, lambda(x, x * x)) << endl;
return 0;
}
Detyrë: Të shtohet metoda ekziston(f)
në klasën Varg<T>
.