Elementi kontrole kopiranja
Svaki puta kada definiramo novi tip tada eksplicitno ili implicitno definiramo njegovo ponašanje prilikom kopiranja, pridruživanja i pri destrukciji. To činimo definiranjem sljedećih članova klase:
-
konstruktor kopiranjem (copy-constructor),
-
konstruktor premještanjem (move-constructor, C++11),
-
operator pridruživanja kopiranjem (copy-assignment operator),
-
operator pridruživanja premještanjem (move-assignment operator, C++11),
-
destruktor.
Konstruktor kopiranjem (CCtor) je konstruktor koji uzima jedan parametar tipa (konstantna)
referenca na tip klase (u kojoj se CCtor nalazi). Koristi se eksplicitno kada se objekt konstruira i inicijalizira
objektom istog tipa, te implicitno kod prijenosa parametara funkciji i u return
naredbi.
Konstruktor premještanjem (MCtor) djeluje u istim situacijama kao i CCtor, ali samo na desnim vrijednostima.
Operator pridruživanja kopiranjem (OP-C) objektu s lijeve strane znaka jednakosti pridružuje vrijednost objekta s desne strane kopiranjem. Objekt na desnoj strani ostaje netaknut.
Operator pridruživanja premještanjem (OP-P) objektu s lijeve strane znaka jednakosti pridružuje vrijednost objekta s desne strane. Objekt na desnoj strani predaje svoje resurse objektu na lijevoj strani i ostaje dobar samo za destrukciju.
Destruktor se poziva automatski kada objekt izlazi iz dosega ili kad se dinamički
alociran objekt briše iz memorije pomoću delete
.
Budući da objekt bilo kojeg tipa mora imati mogućnost kopiranja prevodilac će sintetizirati neke elemente kontrole kopiranja (CCtor, MCtor, OP-C, OP-M i destruktor) ako ih sami ne definiramo.
Konstruktor kopiranjem
Konstruktor kopiranjem ili kraće konstrutor kopije (copy-constructor, CCtor) je konstruktor koji kreira objekt kopiranjem objekta istog tipa. Kao argument uzima referencu na objekt istog tipa.
|
Ako u klasi ne definiramo copy constructor, a u kodu se javlja potreba za njim, prevodilac će ga sintetizirati. |
Kako djeluje sintetizirani CCtor?
Sintetizirani CCtor će izvršiti kopiranje svih nestatičkih varijabli članica tako što će
-
kod ugrađenih tipova kopirati varijablu;
-
kod članica tipa klase pozvati njihov CCtor da obavi kopiranje (definicija je dakle rekurzivna);
-
Ako je polje varijabla članica onda će CCtora obaviti kopiranje polja član-po-član.
Pozivanje CCtora
Poziva u ovim situacijama:
-
Eksplicitna ili implicitna inicijalizacija jednog objekta objektom istog tipa.
-
Kopiranje objekta da bi se predao kao argument funkciji.
-
Kopiranje objekta da bi bio vraćen iz funkcije (kroz return naredbu).
-
Inicijalizacija elementa u sekvencijalnom spremniku.
-
Inicijalizacija elemenata polja iz liste inicijalizatora.
Pogledajmo primjer jednostavne klase MagicNo
koja se može zadovoljiti sa kontrolom kopiranja koju
sintetizira prevodilac.
Primjer:
Jednostavna klasa koja definira samo konstruktor. Konstruktor kopiranjem je sintetizirao prevodilac.
class MagicNo{
public:
MagicNo(const std::string& name, const int& value):
data_name(name), data(value) {}
....
private:
std::string data_name;
int data;
};
// ....
MagicNo no1("Magic number", 117); // Poziv konstruktoru
MagicNo no2(no1); // Poziv konstruktoru kopije
MagicNo no3 = no2; // Poziv konstruktoru kopije
Nakon definicije klase definirali smo tri objekta tipa
MagicNo
.
Prvi je konstruiran konstruktorom koji uzima dva parametra. Drugi i treći su konstruirani CCtorom;
no2
je kopija no1
, a no3
je kopija no2
.
Definicija CCtora
Definirajmo u klasi
MagicNo
jedan CCtor koji se ponaša isto kao i sintetizirani, te jedan konstruktor
koji uzima jedan argument tipa int
i koji se može koristiti u implicitnim konverzijama:
#include <string>
#include <iostream>
#include <vector>
class MagicNo{
public:
MagicNo(const int& value):data_name("NoName"), data(value) // Ctor1
{std::cout << "Ctor1"<<std::endl;}
MagicNo(const std::string& name, const int& value): // Ctor2
data_name(name), data(value) { std::cout << "Ctor2"<<std::endl;}
MagicNo(const MagicNo& obj); // CCtor
private:
std::string data_name;
int data;
};
// u .cpp datoteci
MagicNo::MagicNo(const MagicNo& obj):
data_name(obj.data_name), data(obj.data)
{
std::cout << "CCtor"<< std::endl;
}
Prijenos parametara po vrijednosti
-
Prijenos argumenta tipa klase "po vrijednosti" vrši se tako što CCtor napravi kopiju stvarnog argumenta i taj se privremeni objekt predaje funkciji. Nakon što izađemo iz dosega funkcije privremeni objekt se uništava i pri tome se zove destruktor klase.
-
Kod vraćanja povratne vrijednosti u return naredbi "po vrijednosti" postupak je analogan: prvo CCtor kreira jedan privremeni objekt koji se vraća pozivatelju funkcije primjenom operatora pridruživanja, a onda se privremeni objekt uništava.
Primjer: Imamo funkciju:
MagicNo f(MagicNo A)
{
std::cout << "Unutar funkcije f " << std::endl;
return A;
}
i poziv:
no1 = f(no2);
Dešava se sljedeće:
-
no2
se kopira u lokanu varijabluA
pomoću CCtor-a; -
U
return
naredbi lokalna varijablaA
se kopira pomoću CCtor-a u privremeni bezimeni objekt; -
Privremeni bezimeni objekt se pomoću operatora pridruživanja (
=
) pridružuje varijablino1
; -
Dva privremena objekta se uništavaju pozivima destruktoru.
Optimizacija prevodioca (copy elision)
Što se dešava u ovoj liniji koda?
MagicNo no4 = 3
Prvo se poziva Ctor s jedim argumentom (3
) i kreira se privremeni objekt. Zatim se poziva CCtor
koji kreira no4
. U ovakvoj situaciji prevodioc može izbjeći konstrukciju privremenog
objekta i konstruirati no4
direktno pomoću poziva Ctor-u s jednim argumentom.
Prevodioci to redovito rade. Efekt je kao da smo napisali:
MagicNo no4(3);
Primjer. U čemu je razlika ako pozovemo funkciju
MagicNo f(MagicNo A);
na ovaj način:
no1 = f(no2);
ili na ovaj način:
MagicNo no5 = f(no2);
-
U drugom slučaju će elizija kopiranja (copy elision) dozvoliti da je izbjegne konstrukcija privremenog objekta u
return
naredbi i da se povratna vrijednost direktno konstruira uno5
.
|
g++ ima opciju -fno-elide-constructors pomoću koje se optimizacija konstruktora može spriječiti. |
|
U osnovi kad se vraća lokalni objekt u return naredbi kao u prethodnom primjeru
prevodilac može izbjeći korištenje
konstruktora kopije i direktno konstruirati objekt u memoriji rezerviranoj za dolazni objekt
(no ta optimizacija ovisi o prevodiocu). |
|
U standardu C++-17 elizija kopiranja je postala obavezna u ovakvim situacijama i više nije optimizacija prevodioca. |
Prijenos parametra po referenci
Želimo li izbjeći nepotrebne pozive CCtoru i destruktoru treba parametar funkciji predati "po referenci". Budući da se stvarni argument pri tome ne kopira nema niti poziva CCtoru i Dtoru. Ako definiramo
MagicNo g(const MagicNo& A)
{ std::cout << "Unutar funkcije g " << std::endl; return A;}
Sada kod
std::cout << "Pozivam f" << std::endl;
no2 = f(no1);
std::cout << "f gotov" << std::endl;
std::cout << "Pozivam g" << std::endl;
no2 = g(no1);
std::cout << "g gotov" << std::endl;
daje
Pozivam f
CCtor
Unutar funkcije f
CCtor
operator =
Dtor
Dtor
f gotov
Pozivam g
Unutar funkcije g
CCtor
operator =
Dtor
g gotov
Kopiranje povratne vrijednosti
Kad se radi o povratnoj vrijednosti ovakva optimizacija nije uvijek moguća. Treba zapamtiti sljedeće pravilo:
|
Funkcija nikad ne smije vratiti pokazivač ili referencu na lokalnu varijablu. |
Inicijalizacija spremnika
-
Ako pri inicijalizaciji spremnika zadamo samo broj elemenata, onda se za inicijalizaciju svakog pojedinog člana spremnika koristi defaultni konstruktor.
-
Ako sve elemente inicijaliziramo jednim zadanim elementom, onda se poziva CCtor.
std::cout << "vector s tri elementa:" << std::endl;
std::vector<MagicNo> niz1(3,no1);
std::cout << "vector s tri elementa + konverzija:" << std::endl;
std::vector<MagicNo> niz2(3,4);
std::cout << "polje s tri elementa:" << std::endl;
MagicNo niz3[] ={ no1, no2, no3};
Onemogućavanje kopiranja
Postoje situacije kada ne želimo dozvoliti kopiranje. Objekt tada ne možemo po vrijednosti predati funkciji niti ga iz nje vratiti. Takvi objekti su na primjer streamovi iz iostream biblioteke.
Onemogućavanje kopiranja je vrlo jednostavno. Imamo dva načina:
-
Novi način (C++11): CCtor treba proglasiti obrisanim pomoću ključne riječi
delete
:
class B{
public:
B() : x(0) {}
B(B const &) = delete; // CCtor
private:
int x;
};
Prevodilac će sada javiti grešku (use of deleted function) ukoliko eksplicitno ili implicitno pukušamo pozvati CCtor.
-
CCtor treba staviti u privatni dio klase i ne definirati ga.
class A{
public:
A() : x(0) {}
private:
int x;
A(A const &); // CCtor
};
Prevodilac ili linker će sada javiti grešku ako dođe do poziva CCtora.
-
Specifikacija
=delete
se može koristiti na bilo kojoj funkciji, a ne samo na konstruktorima. Ne treba ju, naravno, nikad koristiti na destruktoru.
Kopiranje klase s pokazivačkom varijablom članicom
Kopiranje postaje problematično u slučaju kada klasa sadrži pokazivač na dnamički alociranu memoriju. Pogledajmo klasu,
class Dangle{
public:
Dangle(){ p= new int(0); }
~Dangle() { delete p; } // Destruktor. Dealokacija memorije.
private:
int * p;
};
Resurse koje instance klase trebaju osiguravamo i inicijaliziramo u konstruktoru, a oslobađamo u destruktoru. To je opći princip konstrukcije klasa i naziva se RAII (Resource Acquisition Is Initialization).
Zamislimo sada situaciju u kojoj kreiramo novi objekt tipa Dangle
iz već postojećeg,
pomoću sintetiziranog CCtora:
void f()
{
Dangle a;
Dangle b = a; // Pozovi sintetizirani CCtor
}
Nakon što oba objekta izađu iz svog dosega pozivaju se njihovi destruktori, koji pozivaju operator
delete
na pokazivaču objekta. Tu dolazi do dvostrukog pozivanja operatora delete
na istom
pokazivaču što predstavlja sigurnu grešku!
Kako ispravno konstruirati takve klase pokazat ćemo malo kasnije.
Operator pridruživanja - kopiranjem
Operator pridruživanja mora biti funkcija članica klase. On se brine za pridruživanje objekata dane klase. Definira se tako da uzima jedan parametar koji je objekt na desnoj strani naredbe pridruživanja (desni operand); taj je parametar prirodno konstantna referenca na tip klase. Povratna vrijednost je referenca na tip klase i ona odgovara lijevom operandu — onom kojem se pridružuje. Vrijedi pravilo:
|
Ako u klasi ne definiramo operator pridruživanja, a u kodu se javlja potreba za njim, prevodilac će ga sintetizirati. |
Sintetizirani OP vrši pridruživanje član po član na način kompatibilan sa sintetiziranim CCtorom. Svi ugrađeni tipovi bit će kopirani; polja će biti kopirana član-po-član kao i u CCtoru. Članice korisničkog tipa pridružuju se pozivanjem operatora pridruživanja iz njihove klase. Taj može biti eksplicitno definiran ili sintetiziran.
Primjer:
Ovdje klasi MagicNo
dodajemo operator
pridruživanja koji djeluje kao sintetizirani OP.
Nastavak
class MagicNo{
public:
MagicNo(const int& value):data_name("NoName"),
data(value) // Ctor1
{std::cout << "Ctor1"<<std::endl;}
MagicNo(const std::string& name, const int& value): // Ctor2
data_name(name), data(value)
{ std::cout << "Ctor2"<<std::endl;}
MagicNo(const MagicNo& obj); // CCtor
MagicNo& operator=(const MagicNo& obj); // OP
private:
std::string data_name;
int data;
};
// u .cpp datoteci
MagicNo& MagicNo::operator=(const MagicNo& obj)
{
data_name = obj.data_name;
data = obj.data;
std::cout << "operator = "<< std::endl;
return *this;
}
Sintaksa
Sintaksa poziva operatora pridruživanja je prirodna. U liniji
no1 = no2;
vrijednost objekta no2
pridružuje se objektu no1
. Ta je sintaksa
samo pokrata za ekvivalentnu sintaksu:
no1.operator=(no2);
OP vraća nekonstantnu referencu
MagicNo no1("Number 1", 117);
MagicNo no2("Number 2", 63);
MagicNo no3("Number 3", 249);
// ....
(no2 = no1) = no3;
Zadatak: Što bi se desilo u gornjem kodu da smo OP definirali tako da vraća objekt umjesto reference?
MagicNo operator=(const MagicNo& obj); // OP, GREŠKA
Prevodilac može odbiti sintetizirati OP
Pravilo je da će prevodilac sintetizirati OP ako ga nismo definirali. To pravilo ima iznimaka, tj. postoje situacije u kojima će prevodilac odbiti sintetizirati OP za nas. To će se desiti u situacijama kada ponašanje sintetiziranog OP nije jednoznačno.
Tri situacije u kojima će prevodilac odbiti sintetizirati OP su sljedeće:
-
klasa ima članicu koja je referenca na objekt ili
-
klasa ima konstantnu varijablu članicu ili
-
klasa ima članicu koja ima nedostupni (privatni) ili obrisani OP.
C++11: U gornjim situacijama prevodilac će sintetizirati obrisani OP. Ukupni efekt je isti, jedino što će prilikom pokušaja korištenja OP prevodilac javiti da je OP obrisan (deleted).
Slično pravilo vrijedi i za defaultni konstruktor: Sintetizirani defaultni konstruktor će biti obrisan u sljedećim situacijama:
-
Klasa ima varijablu članicu s obrisanim ili nedostupnim dodijeljenim konstruktorom;
-
Klasa ima članicu koja je referenca ali nije inicijalizirana u klasi.
-
Klasa ima konstantnu članicu koja nema dodijeljeni konstruktor i nije inicijalizirana unutar klase.
Konstruktor kopije će biti sintetiziran kao obrisan samo ako klasa ima članicu koja ima nedostupan/obrisan CCtor pa se ne može kopirati.
Destruktor će biti sintetiziran kao obrisan samo ako klasa ima članicu koja ima nedostupan/obrisan destruktor.
OP i CCtor dolaze u paru
Ako je zadovoljavajući sintetizirani CCtor, onda je zadovoljavajući i sintetizirani OP, i obratno. Kada moramo definirati svoj OP onda redovito moramo definirati i CCtor, i obratno.
Pogledajmo na kraju kako bi trebalo korigirati klasu Dangle
da postane ispravna:
class NoDangle{
public:
NoDangle() : p(new int(0)) {}
NoDangle(const NoDangle& rhs) : p(new int(*rhs.p)) {}
NoDangle& operator=(NoDangle &rhs){
if(rhs.p != p) {
delete p;
p = new int(*rhs.p);
}
return *this;
}
~NoDangle() { delete p; }
private:
int * p;
};
-
Operator pridruživanja kopira vrijednost na koju pokazivač pokazuje, a ne sam pokazivač (duboko kopiranje).
-
Operator pridruživanja mora brinuti o trivijanom pridruživanju (
a=a
).
Destruktor
Destruktor je posebna funkcija članica klase koja se poziva automatski kod destrukcije objekta. Zadatak destruktora je osloboditi resurse koje je objekt rezervirao (file handles, socets, itd.), osloboditi dinamički alociranu memoriju itd.
-
Destruktor ima isto ime kao i klasa kojem prethodi tilda (
~
). Nema povratne vrijednosti i ne uzima parametre.
Kada se poziva destruktor?
-
Kada je doseg objekta lokalan destruktor se poziva u trenutku kada objekt izlazi iz svog dosega.
-
Ako je objekt kreiran pomoću operatora
new
, destruktor će biti pozvan kad se pozove operatordelete
.
Važno je uočiti da se destruktor ne poziva kada referenca ili pokazivač izlazi iz
dosega. To se dešava samo kada objekt (a ne referenca) izlazi iz dosega i
kada se pozove delete
na pokazivaču.
Spremnici kao što su polja i STL spremnici uništavaju se kada izlaze iz dosega.
Pri tome, ako su im članovi tipa klase, onda se poziva destruktor na svakom članu
spremnika, u redosljedu suprotnom od onog kojim su elementi konstruirani.
Isti postupak se dešava s dinamički alociranim poljima na kojima
je pozvan delete []
.
Sintetizirani destruktor
|
Ako ne definiramo vlastiti destruktor prevodilac će uvijek sintetizirati destruktor za nas. |
Sintetizirani destruktor uništava nestatičke objekte u poretku suprotnom od onog u kojem su konstruirani, a to znači u poretku suprotnom od onog kojim su u klasi deklarirani. Na svakom članu tipa klase poziva njegov destruktor.
Pisanje vlastitog destruktora
-
Klase koje ne alociraju resurse (memoriju i druge resurse) ne moraju definirati svoj destruktor. Sintetizirani će biti dovoljan da obavi posao.
-
Pravilo trojice: ako imamo potrebu napisati eksplicitni destruktor, onda moramo napisati i CCtor i OP.
Primjer (ovdje nemamo potrebu za destruktorom, samo ilustriramo sintaksu):
Nastavak
class MagicNo{
public:
MagicNo(const int& value):data_name("NoName"), data(value) // Ctor1
{std::cout << "Ctor1"<<std::endl;}
MagicNo(const std::string& name, const int& value): // Ctor2
data_name(name), data(value) { std::cout << "Ctor2"<<std::endl;}
MagicNo(const MagicNo& obj); // CCtor
MagicNo& operator=(const MagicNo& obj);
~MagicNo(){ std::cout << "Dtor"<<std::endl;}
private:
std::string data_name;
int data;
};
Prazna klasa nije prazna
Ako definiramo praznu klasu
class Empty {};
dobivamo klasu oblika:
class Empty{
Empty(){...}
Empty(const Empty& rhs){...}
~Empty(){...}
Empty& operator=(const Empty& rhs){...}
};
Primjer: Vektor varijabilne duljine: predložak klase Vec
Predložak Vec<T>
sadržava dinamički
alociran vektor proizvoljne duljine. Polje alociramo dinamički u
konstruktoru, a dealociramo ga u destruktoru. Imamo još jednu dodatnu varijablu mime
koja može predstavljati ime fizičke veličine koju vektor drži.
template <typename T>
class Vect
{
public:
using Index = unsigned int;
explicit Vect(Index n = 0, T v=0.0, std::string ime="");
~Vect(){ delete [] mdata; std::cout << "Dtor" << std::endl; }
Vect(const Vect& v); // CCtor
Vect& operator=(const Vect& v); // OP
T operator[](Index i) const { return mdata[i]; }
T& operator[](Index i) { return mdata[i]; }
Index size() const { return msize; }
T two_norm() const;
std::string get_name() const { return mime; }
private:
Index msize;
T *mdata;
std::string mime;
};
Dealokacija
Dealokacija memorije se dešava kod poziva destruktora, dakle kada objekt izlazi iz dosega ili
kada se pozove delete
na dinamički alociranom objektu.
int main()
{
Vec<double> x(5,2.0);
Vec<double> * px = new Vect(1024,1.9);
delete px; // Poziv destruktor iz klase Vect.
// x izlazi iz dosega -> poziv destruktora iz klase Vect.
return EXIT_SUCCESS;
}
Destruktor je dan u samoj klasi (i stoga je implicitno inline
):
~Vect(){ delete [] mdata; std::cout << "Dtor" << std::endl; }
Konstruktor kopije
Dizajn konstruktora kopije posve je izravan. Alociramo novu memoriju za vektor i kopiramo članove iz danog vektora.
template <typename T>
Vect<T>::Vect(const Vect& v): msize(v.size()), mdata(new T[v.size()]), mime(v.get_name())
{
std::cerr << "C-Ctor"<<std::endl;
for(Index i=0; i < msize; ++i)
mdata[i]=v.mdata[i];
}
Operator pridruživanja
Kod operatora pridruživanja moramo paziti na dvije stvari:
-
Da dealociramo memoriju vektora na lijevoj strani.
-
Da dobro tretiramo samopridruživanje.
template <typename T>
Vect<T>& Vect<T>::operator=(const Vect& v)
{
std::cerr << "OP"<<std::endl;
if(mdata != v.mdata) // Vektori su jednaki ako pokazuju na istu memorijsku lokaciju
{
delete [] mdata;
mdata = new T[v.msize]; // bad_alloc ako ne uspije
msize = v.msize;
mime = v.mime;
for(Index i=0; i < msize; ++i)
mdata[i]=v.mdata[i];
}
return *this;
}
Zadatak. Implementirajte sve ostale metode iz klase Vec<T>
.
Optimizirajte operator pridruživanja tako da ne dealocira memoriju ako to nije nužno.
Valgrind
-
Zaboravimo li smjestiti dealokaciju u destruktor imat ćemo problem gubitka memorije (eng. memory leak). Dinamički alocirana memorija ne oslobađa se u trenutku kada gubimo pristup do nje već ostaje alocirana do kraja izvršavanja programa.
Gubitak memorije predstavlja ozbiljan problem u C++ programima. Stoga su razvijeni različiti alati koji ga mogu detektirati. U Linux okruženju može se koristiti valgrind program http://www.valgrind.org/. Izvršavanje programa (recimo a.out) vrši se "kroz" valgrind koji prati izvršavanje programa i izvještava o greškama u radu s memorijom:
valgrind --tool=memcheck --leak-check=yes ./a.out
Pretpostavimo da smo iz klase Vect
uklonili destruktor. Dio poruke koju valgrind daje mogla
bi izgledati ovako:
==4128== 8,272 bytes in 3 blocks are definitely lost in loss record 1 of 1
==4128== at 0x4024B9C: operator new[](unsigned) (vg_replace_malloc.c:195)
==4128== by 0x804921C: Vect::Vect(int, double, std::string) (in /home/jurak/mytext/{cxx}/klase-pr/a.out)
==4128== by 0x8048B5A: main (in /home/jurak/mytext/{cxx}/klase-pr/a.out)
==4128==
==4128== LEAK SUMMARY:
==4128== definitely lost: 8,272 bytes in 3 blocks.
==4128== possibly lost: 0 bytes in 0 blocks.
==4128== still reachable: 0 bytes in 0 blocks.
==4128== suppressed: 0 bytes in 0 blocks.
Vidimo da valgrind detektira klasu koja je alocirala memoriju koja nije dealocorana.
Konstrukcija i pridruživanje premještanjem - problem
-
Problem. Kopiranje podataka koje vrše CCtor i OP je nepotrebno (i stoga neefikasno) u slučaju kada se objekt koji se kopira neposredno nakon toga uništava. Tada je efikasnije (i prirodnije) preuzeti resurse tog objekta umjesto da ih se kopira jer će na objektu ionako u sljedećem koraku biti pozvan destruktor. Posao preuzimanja resursa obavljaju konstruktor kopije premještanjem i operator pridruživanja premještanjem.
Primjer 1. Napravimo prvo mali test program koji mjeri (posredno) vrijeme potrebno za kreiranje i kopiranje lokalnog objekta.
#include <chrono> // za mjerenje vremena
#include "vector.h"
// Funkcija koja vraća lokalnu kopiju
template <typename T>
Vec<T> make_square(Vec<T> const & in) {
Vec<T> y(in.size());
for(typename Vec<T>::index i = 0; i < in.size(); ++i)
{ y[i] = in[i]*in[i]; }
return y;
}
using namespace std::chrono;
int main()
{
Vec<double> x(500000,2.0), y;
auto start = system_clock::now(); // Pokreni sat.
y = make_square(x);
auto stop = system_clock::now(); // Ponovo startanje sata
auto duration = duration_cast<microseconds>( stop - start); // vremenska razlika
std::cout << "Time for the call = " << duration.count()
<< " micro sec" << std::endl; // u mikrosekundama
return 0;
}
Rezultat izvršavanja je:
Ctor
Ctor
Ctor
OP
Dtor
Time for the call = 6898 micro sec
Dtor
Dtor
Ovdje je došlo do elizije CCtora u return
naredbi.
Napomena. Neefikasnost ovog koda je u pozivu operatoru pridruživanja koji kopira vektor y
, lokalno definiran unutar
metode make_square
, u vektor y
definiran unutar metode main
. Vektor
y
iz metode make_square
je iskopiran član po član u vektor y
unutar metode main
,
a zatim je uništen prolazom destruktora. Prirodnije je da vektor y
unutar metode main
jednostavno preuzme dinamički alociranu memoriju vektora y
iz metode make_square
jer se na taj način
izbjegava nepotrebna alokacija i dealokacija memorije.
Nastavak
-
Nepotrebno kopiranje se dešava i u STL spremnicima kao što je
std::vector
koji dinamički povećavaju svoju veličinu kada je to potrebno. Ako ustd::vector
pokušamo ubaciti element kada u njemu više nema slobodnih mjesta on će alocirati novu memoriju dovoljne veličine i prekopirati već postojeće element u nju. To kopiranje je neefikasno i može se zamijeniti premještanjem.
Primjer 2: Proučite broj poziva konstruktoru kopije i destruktoru u sljedećem kodu:
Vect<double> a1(3,4.0), a2(3,2.0), a3(4,1.0), a4(4,-1.0);
std::vector<Vect<double>> collection;
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a1);
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a2);
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a3);
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a4);
std::cout << "capacity = " << collection.capacity() << std::endl;
Rezultat:
Ctor
Ctor
Ctor
Ctor
capacity = 0
Ubaci element:
C-Ctor
capacity = 1
Ubaci element:
C-Ctor
C-Ctor
Dtor
capacity = 2
Ubaci element:
C-Ctor
C-Ctor
C-Ctor
Dtor
Dtor
capacity = 4
Ubaci element:
C-Ctor
capacity = 4
Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
Objašnjenje. Vektor počinje svoj život kao prazan. Kad ubacimo prvi element u njega on alocira
memoriju za jedan element i kopira element u svoju memoriju (poziv C-Ctoru). Kada ubacujemo drugi element
za njega nema mjesta u vektoru. Tada std::vector
alocira memoriju za dva objekta, kopira prvi
element na novu memorijsku lokaciju i zatim kopira novoubačeni element iza prvog elementa. Prvi element na staroj
lokaciji se uništava pozivom destruktora. Kod ubacivanja
trećeg elementa ponovo nema dovoljno mjesta u vektoru i postupak se ponavlja. Sada se alocira memorija za 4
elemenata na novoj lokaciji, prva dva elementa se kopiraju sa stare lokacije i ubacuje se treći element.
Prva dva elementa na staroj lokaciji se uništavaju prolazom destruktora.
Sada za četvrti element imamo slobodno mjesto pa pri njegovom ubacivanju nema nove realokacije. Jasno je da su
u slučaju objekata (Vect
) koji dinamički alociraju memoriju sva ta silna kopiranja nepotrebna i neefikasna.
Konstruktor kopije premještanjem i operator pridruživanje premještanjem
Neefikasnost u prehodna dva primjera (i drugim) možemo izbjeći dodavanjem dva nova operatora svakom tipu kod kojeg premještanje može biti efikasnije od kopiranja.
-
Konstruktor kopije premještanjem (eng. move constructor).
-
Operator pridruživanje premještanjem (eng. move assignment operator).
Obje ove funkcije uzimaju argument tipa desne reference što znači da djeluju na privremenim bezimenim objektima koji su na kraju svog životnog vijeka i stoga se njihovi resursi mogu preuzeti.
Puna kontrola kopiranja u klasi Vect ima ovaj oblik:
template <typename T>
class Vec
{
public:
using Index = std::size_t;
explicit Vect(Index n = 0, T v=0.0, std::string ime="");
~Vec(){ std::cerr << "Dtor"<<std::endl; delete [] mdata;}
Vec(const Vec& v); // CCtor
Vec(Vec && v) noexcept; // MCtor
Vec& operator=(const Vec& v); // OP
Vec& operator=(Vec && v) noexcept; // MOP
// ...
};
-
Konstruktor kopije premještanjem uzima nekonstantnu referencu na desnu vrijednost. To znači da će prevodilac:
-
Pozvati konstruktor kopije kopiranjem ako kopira lijevu vrijednost;
-
Pozvati konstruktor kopije premještanjem ako kopira desnu vrijednost.
-
-
Operator pridruživanja kopiranjem ili premještanjem selektira se na isti način, ovisno o tome je li na desnoj strani lijeva ili desna vrijednost.
-
Da bi se mogli efikasno koristiti sa spremnicima konstruktor kopije i operator pridruživanja premještanjem moraju biti označeni
noexcept
— što znači da ne izbacuju izuzetke.
Konstruktor kopije premještanjem: Implementacija
-
Novokreirani vektor preuzima resurse vektora kojeg kopira preuzimanjem.
-
Objekte (kao npr.
std::string
) preuzimamo pomoćustd::move
funkcije. -
Objekt s desne strane mora ostati u stanju pogodnom za prolaz destruktora (pokazivač obavezno na
nullptr
).
template <typename T>
Vect<T>::Vect(Vect && v) noexcept : msize(v.msize), mdata(v.mdata), mime(std::move(v.mime))
{
std::cerr << "M-Ctor"<<std::endl;
v.msize = 0;
v.mdata = nullptr;
}
Operator pridruživanje premještanjem: Implementacija
-
Resursi se preuzimaju na isti način kao i u konstruktoru.
-
Moramo paziti na samopridruživanje zbog koda oblika:
x = std::move(x);
-
Objekt s desne strane mora ostati u stanju pogodnom za prolaz destruktora (pokazivač obavezno na
nullptr
).
template <typename T>
Vect<T>& Vect<T>::operator=(Vect && v) noexcept
{
std::cerr << "M-OP"<<std::endl;
if(this != &v) // samopridruživanje je moguće ako se koristi std::move
{
delete [] mdata;
mdata = v.mdata;
msize = v.msize;
mime = std::move(v.mime);
v.mdata = nullptr;
v.msize = 0;
}
return *this;
}
Primjer 1. Isti kao i ranije:
#include <chrono> // za mjerenje vremena
template <typename T>
Vec<T> make_square(Vec<T> const & in)
{
Vec<T> y(in.size());
for(typename Vec<T>::index i = 0; i < in.size(); ++i)
{ y[i] = in[i]*in[i]; }
return y;
}
using namespace std::chrono;
int main()
{
Vec<double> x(500000,2.0), y;
auto start = system_clock::now(); // Pokreni sat.
y = make_square(x);
auto duration = duration_cast<microseconds>(
system_clock::now() - start
); // vremenska razlika
std::cout << "Time for the call = " << duration.count()<< " micro sec" << std::endl; // u milisekundama
return EXIT_SUCCESS;
}
Rezultat izvršavanja je:
Ctor
Ctor
Ctor
M-OP
Dtor
Time for the call = 5111 micro sec
Dtor
Dtor
Budući da tip koji vraćamo iz funkcije make_square
ima operator pridruživanja premještanjem
on je iskorišten umjesto operatora pridruživanja kopiranjem. Time smo uštedjeli dealokaciju i alokaciju
500000 elemenata tipa double
.
Primjer 2. Spremanje u std::vector
: U programu
Vect<double> a1(3,4.0), a2(3,2.0), a3(4,1.0), a4(4,-1.0);
std::vector<Vect<double>> collection;
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a1);
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a2);
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a3);
std::cout << "capacity = " << collection.capacity() << std::endl;
std::cout << "Ubaci element:\n";
collection.push_back(a4);
std::cout << "capacity = " << collection.capacity() << std::endl;
sada dobivamo sljedeći rezultat iz kojeg je vidljivo da vektor vrši realokaciju pomoću konstruktora kopije premještanjem:
Ctor
Ctor
Ctor
Ctor
capacity = 0
Ubaci element:
C-Ctor
capacity = 1
Ubaci element:
C-Ctor
M-Ctor
Dtor
capacity = 2
Ubaci element:
C-Ctor
M-Ctor
M-Ctor
Dtor
Dtor
capacity = 4
Ubaci element:
C-Ctor
capacity = 4
Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
Iz ispisa vidimo da pri premještanju starih elemenata na nove lokacije std::vector
koristi
operator pridruživanja premještanjem i na taj način štedi na alokaciji i dealokaciji memorije.
Napomena: std::vector
garantira da polazni vektor neće biti
promijenjen ako realokacija elemenata ne uspije. To je lako osigurati s kopiranjem jer kopiranje ne mijenja polaznu vrijednost.
Da bi se moglo
osigurati s premještanjem, konstruktor kopije premještanjem i operator pridruživanja premještanjem moraju
garantirati da neće izbaciti izuzetak.
Ukoliko nema takve garancije std::vector
će pri realokaciji koristiti konstruktor kopije kopiranjem i
operator pridruživanja kopiranjem.
Zbog toga operatori premještanjem moraju biti deklarirani noexcept
ukoliko želimo da budu efikasno iskorišteni.
Zaključci, sintetizirano premještanje:
-
Mehanizam koji omogućava kontrolu kopiranja kopiranjem-premještanjem je preopterećenje funkcija u odnosu na lijevu i desnu referencu. Pri tome uvijek kombiniramo funkciju koja uzima konstantnu lijevu referencu i nekonstantnu desnu referencu.
-
Ako konstruktor kopije ili operator pridruživanja ne postoje u verziji s premještanjem koristi se verzija s kopiranjem (jer se konstantna lijeva referenca može inicijalizirati desnom vrijednošću).
-
I sve druge funkcije se mogu preopteretiti po smislu reference (lijeva/desna), posebno i druge članice klase pored CCtor-a i OP.
-
std::move
treba koristiti vrlo racionalno je o stanju objekta nakon što je pomaknut znamo samo da je dobar za destrukciju. -
Prevodilac sintetizira konstruktor i OP premještanjem samo ako klasa ne definira niti jedan član kontrole kopiranja i ako sve varijable članice mogu biti konstruirane premještanjem odnosno pridružene premještanjem.
-
Ako eksplicitno zatražimo sintetiziranje operacija premještanjem pomoću
= default
, a prevodilac nije u mogućnosti premjestiti članove klase, operacija premještanjem će biti definiranadeleted
.
Još o povratnoj vrijednosti funkcije
-
Ima li smisla definirati funkciju tako da vraća desnu referencu?
Vect && f(Vect & x){
Vect y = x.scale(2);
return std::move(y);
}
Vect z = f(x); // Greška
Vect && z1 = f(x); // Greška
-
Ako funkcija vraća referencu na lokalnu varijablu to je greška (kao u slučaju da vraća lijevu referencu). Lokalni objekt je uništen prije povratka u pozivni program.
Jedina mogućnost takvog koda je ovog tipa:
Vect && g(Vect && x){
x.scale(2);
return std::move(x);
}
x = g(std::move(x)); // OK
-
Treba li koristiti
std::move()
pri povratu lokalnog objekta?
Vect f(Vect & x){
Vect y = x.scale(2);
return std::move(y);
}
Vect z = f(x);
-
Odgovor je NE. Prevodilac ima pravo primijeniti operator kopiranja premještanjem na lokalnu varijablu i
std::move()
je suvišan. Štoviše,std::move()
će spriječiti optimizaciju povratne vrijednosti koja može ponekad eliminrati i kopiranje i premještanje.
Klase s pokazivačima
Problem
Programski jezik C++ nema sakupljača smeća (eng. garbage collector) kao neki drugi jezici (npr. Java) i stoga programer mora brinuti o dealokaciji dinamički alocirane memorije. Sljedeći primjer pokazuje jednu factory metodu. To je metoda koja dinamički alocira objekt i vraća pokazivač na njega.
class BFC{
public:
explicit BFC(int a = 0) : aa(a) {}
int get() const { return aa; }
void set(int a) { aa=a; }
// .......
~BFC() { cout << "BFC(" << get() << ") is dead."<< endl; }
private:
int aa;
// ....
};
BFC * factoryBFC(int i)
{
// ...
// Dinamički alociramo objekt
BFC * pbfc = new BFC(i);
// ...
// Vraćamo pokazivač na dinamički alociran objekt
return pbfc;
}
Problem - moramo brinuti o dealokaciji
Odgovornost je pozivnog programa da dealocira memoriju kada je gotov s objektom koji je dobio od factory metode.
void f()
{
BFC *a = factoryBFC(1);
BFC *b = factoryBFC(2);
// ....
cout << "a = " << a->get() << endl;
cout << "b = " << b->get() << endl;
// Dealociram memoriju alociranu u factoryBFC
delete a;
// Zaboravio uništiti b ! -- memory leak
}
-
Dinamičko alociranje objekata je često te je potrebno naći sustavno rješenje za dealokaciju.
-
Rješenje problema: Pokazivač treba smjestiti unutar klase čiji destruktor poziva
delete
na pokazivaču. Kada takav objekt izađe iz dosega automatski se poziva destruktor koji dealocira objekt na koji pokazivač referira. Na taj način ne moramo brinuti o dealokaciji memorije.
Napomena. Ovo rješenje je primjer programskog principa koji se naziva Resource Acquisition Is Initialization (RAII). Prema tom principu objekt prikuplja potrebne resurse u konstruktoru, a oslobađa ih u destruktoru, što automatizira oslobađanje resursa.
unique_ptr<T>
Standardna biblioteka nam nudi klasu unique_ptr<T>
koju možemo koristiti za sprečavanje gubljenja memorije. Dekarirana je u
zaglavlju <memory>
.
Klasu koristimo na sljedeći način: Ako imamo pokazivač na tip T
za koji želimo da se
automatski dealocira pri izlasku iz dosega predajemo ga konstruktoru klase
unique_ptr<T>
.
Objekt klase unique_ptr<T>
koristimo na isti
način kao i pokazivač. To je moguće stoga što je klasa unique_ptr<T>
tzv. pokazivački objekt, odnosno u njoj su preopterećeni operatori *
i ->
.
void f()
{
unique_ptr<BFC> a( factoryBFC(1) );
unique_ptr<BFC> b( factoryBFC(2) );
// ....
cout << "a = " << a->get() << endl;
cout << "b = " << b->get() << endl;
// Dealokacija memorije je sada automatska
}
Na izlasku iz funkcije f()
više ne moramo brinuti o dealokaciji memorije. Nju će obaviti destruktor klase
unique_ptr<BFC>
koji poziva delete
na pokazivaču.
Pametni pokazivač
Objekt koji možemo koristiti kao pokazivač (dereferencirati ga i primijeniti operator → na njega) naziva se pametni pokazivač. Pametan je jer obično ima dodatne sposobnosti koje pokazivači nemaju. Kažemo još da su pametni pokazivači objekti sa semantikom pokazivača.
unique_ptr<T> i kopiranje
Klasa unique_ptr<T>
implementira koncept ekskluzivnog vlasništva nad objektom.
-
Može postojati samo samo jedan
unique_ptr<T>
koji drži zadani pokazivač naT
.
To se postiže time što unique_ptr<T>
nema konstruktora kopije kopiranjem niti
operatora pridruživanja kopiranjem.
Konstrukcija
-
Dodijeljeni konstruktor kreira neinicijalizirani pametni pokazivač (onaj koji drži
nullptr
). -
Operator konverzije u
bool
dozvoljava ispitivanje je li objekt prazan ili drži netrivijalni pokazivač.
std::unique_ptr<int> up1, up11(nullptr); // objekti bez pokazivača
std::unique_ptr<int> up2(new int(4));
std::cout << "*up2 = " << *up2 << "\n";
if(up1)
std::cout << "*up1 = " << *up1 << "\n";
else
std::cout << "up1 je prazan.\n";
-
Ako se ivrši premještanje pametnog pokazivača, pokazivač na desnoj strani prepušta resurs pokazivaču na lijevoj strani. Pokazivač na lijevoj strani prijue toga oslobađa svoj resurse (poziva
delete
na svom pokazivaču).
up1 = std::move(up2);
assert(up2 == nullptr);
Ekvivalentan kod je
up1.reset(up2.release());
assert(up2 == nullptr);
Imamo sljedeće metode:
Poziv | Značenje |
---|---|
|
Vraća pokazivač koji |
|
Vraća pokazivač koji drži |
|
Briše objekt na koji |
|
Briše objekt na koji |
unique_ptr<T> i funkcije
-
Funkcija koja uzima
std::unique_ptr<>
po vrijednosti preuzima vlasništvo nad objektom. Pri pozivu moramo koristitistd::move()
. -
Funkcija koja samo manipulira sa objektom može to raditi kroz goli pokazivač. Treba samo slijediti konvenciju da goli pokazivač indicira da funkcija nije vlasnik objekta.
-
Funkcija koja kreira i vraća
std::unique_ptr<>
odriče se vlasništva nad kreiranim objektom.
// Funkcija konzumira i uništava UP.
void f(std::unique_ptr<int> pi){
std::cout << "pi drži adresu " << pi.get() << "\n";
}
// Funkcija prima goli pokazivač, što znači da NIJE vlasnik objekta.
void f(int *pi){
std::cout << "pi drži adresu " << pi << "\n";
}
// Funkcija se odriče vlasništva nad UPom.
std::unique_ptr<int> g(int n){
std::unique_ptr<int> pn(new int(n));
return pn;
}
Te funkcije koristimo na ovaj način:
f(std::move(up1)); // umjesto CCtora koristimo M-Ctor
assert(up1 == nullptr);
f(up2.get()); // predaj goli pokazivač
assert(*up2 == 5);
up2 = g(5);
assert(*up2 == 5);
unique_ptr<T> kao članica klase
Običan pokazivač na dinamički alociranu memoriju unutar klase ima ovaj nedostatak:
-
Ako je izuzetak izbačen u konstruktoru prije no što je cijeli objekt izgrađen, destruktor se ne poziva. Pozivaju se destruktori samo onih članica klase koje su izgrađene.
unique_ptr<T>
kao članica klase umjesto golog pokazivača garantira da će memorija biti dealocirana
i ako je izbačen izuzetak u konstruktoru. Problem je tada sljedeći:
-
Ako u klasi držimo
unique_ptr<T>
moramo implementirati kopiranje i pridruživanje.
class X{
std::unique_ptr<int> pi;
std::unique_ptr<char> pc;
public:
X(int i, char c) : pi(new int(i)), pc(new char(c)) {}
X(X const & x) : pi(new int(*x.pi)), pc(new char(*x.pc)) {}
X & operator=(X const & x){
*pi = *x.pi; // duboko kopiranje
*pc = *x.pc;
return *this;
}
// destruktor nije potreban
};
unique_ptr<T> i polja
-
Destruktor u
unique_ptr<T>
pozivadelete
operator, a nedelete []
operator. Stoga se ne koristi sa dinamički alociranim poljima. -
Kada želimo imati
unique_ptr
koji pokazuje na dinamički alocirano polje treba koristiti klasuunique_ptr<T[]>
. Tadada možemo koristiti operator uglatih zagrada ([]
), ali ne postoje operatori*
i->
. Na primjer:
std::unique_ptr<double[]> polje(new double[5]);
polje[3] = 17.0;
std::cout << "Adresa polja = " << polje.get()
<< "; polje[3] = " << polje[3] << std::endl;
unique_ptr<T>. Završne napomene
Klasa unique_ptr<T>
ima dodatni argument predloška koji ima dodijeljenu vrijednost.
namespace std {
// primary template:
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
...
T& operator*() const;
T* operator->() const noexcept;
...
};
}
Klasa default_delete<T>
poziva delete
na pokazivaču. Zadavanjem parametra D
to je ponašanje
moguće izmijeniti.
-
unique_ptr<T>
nam garantira da nikad neće postojati drugi pametni pokazivač koji drži isti pokazivač. Taj je pokazivač jedinstven i garantirana je dealokacija memorije kad izađe iz dosega.unique_ptr<T>
implementira vlasničku semantiku. -
Klasa
unique_ptr<T>
uvedena je u standardnu biblioteku standardizacijom iz 2011. godine. Prije toga je istu ulogu vršila klasaauto_ptr<T>
koja i danas postoji u standardnoj biblioteci, no ne treba ju koristiti. -
Objekte tipa
unique_ptr<T>
možemo stavljati u STL spremnike, za razliku odauto_ptr<T>
objekata koje ne možemo.
Brojanje referenci
Ponekad, radi efikasnosti, imamo više pokazivača koji referiraju na isti objekt. Kada trebamo novu kopiju samo kopiramo pokazivač.
Kada je takvo ponašanje poželjno
onda trebamo riješiti problem višestruke dealokacije objekta. Ponovo ćemo pokazivač
zatvoriti u jednu klasu no u destruktoru klase nećemo nužno pozivati delete
na
pokazivaču. Tek kada posljednji pokazivač na objekt izlazi iz dosega, i ne ostaje niti
jedan koji referira na dani objekt, zovemo delete
.
Tehnika koja nam omogućava držanje više pametnih pokazivača s adresom istog objekta zove se brojanje referenci i implementirana je u ovoj klasi:
class SmartBFC{
public:
SmartBFC(BFC *p) : ptr(p), cnt(new int(1)) {}
SmartBFC(const SmartBFC & orig): ptr(orig.ptr), cnt(orig.cnt)
{ ++*cnt; }
SmartBFC& operator=(const SmartBFC&);
~SmartBFC(){ if(--*cnt == 0) { delete ptr; delete cnt; } }
// Pomoćne funkcije
const BFC * get() const { return ptr; }
int use_count() const { return *cnt; }
private:
BFC * ptr;
int * cnt;
};
Naša implementacija dinamički alocira brojač referenci:
-
Klasa
SmartBFC
sadrži pokazivačptr
na objekt tipaBFC
i pokazivačcnt
na brojač instanci klaseSmartBFC
koje pokazuju na isti objekt; -
Konstruktor uzima pokazivač na (dinamički alociran) objekt tipa
BFC
i njime inicijalizira svoj pokazivač. Ujedno na hrpi kreira objekt tipaint
koji inicijalizira jedinicom te inicijaliziracnt
s pokazivačem na njega. Nakon što konstruktor završi brojač referenci je postavljen na jedinicu.
Napomena: Klasa SmartBFC
nije pametni pokazivač jer ne implementira semantiku pokazivača.
Te ćemo detalje popraviti naknadno.
Brojanje referenci - konstruktor kopiranjem
Kopiranje SmartBFC objekta:
SmartBFC(const SmartBFC & orig): ptr(orig.ptr), cnt(orig.cnt) { ++*cnt; }
-
Konstruktor kopije jednostavno kopira pokazivače i povećava brojač referenci za jedan.
~SmartBFC(){ if(--*cnt == 0) { delete ptr; delete cnt; } }
-
Destruktor smanjuje brojač za jedan i ako je umanjeni brojač nakon toga jedanak nuli (to znači da nema više niti jednog
SmartBFC
objekta koji pokazuje na daniBFC
objekt) dealocira i objekt na koji pokazuje i brojač. Ako nakon smanjivanja brojača on nije jednak nuli destruktor ne radi ništa. U tom slučaju ima jošSmartBFC
objekata koji pokazuju na promatrani objekt i koriste brojač.
const BFC * get() const { return ptr; }
int use_count() const { return *cnt; }
-
Pomoćna funkcija
get()
nam omogućava da dohvatimo pokazivač na pohranjeni objekt. Funkcijause_count()
daje brojač "referenci" na objekt.
Brojanje referenci - operator pridruživanja
Implementacija operatora pridruživanja:
SmartBFC& SmartBFC::operator=(const SmartBFC& rhs)
{
++*rhs.cnt;
if(--*cnt == 0) { delete ptr; delete cnt; }
ptr = rhs.ptr;
cnt = rhs.cnt;
return *this;
}
Ovdje, da bismo se zaštitili od "samopridruživanja" prvo povećavamo brojač desne strane,
a onda smanjujemo brojač lijeve strane. Dealokaciju objekta na lijevoj strani vršimo
jedino ako je umanjeni brojač jednak nuli. Nakon toga kopiramo pokazivače. Uočimo da će
kod pridruživanja oblika a=a
doći prvo do povećanja, a zatim smanjenja brojača,
te se tako izbjegava moguća dealokacije objekta.
Primjena
int main()
{
SmartBFC spt1(new BFC(13));
cout << "cnt (u spt1) =" << spt1.use_count() << endl; // 1
SmartBFC spt2 = spt1; // CCtor
cout << "cnt (u spt1) = " << spt1.use_count() << endl; // 2
cout << spt1.get()->get() << endl;
SmartBFC spt3(new BFC(33));
spt1 = spt3;
return 0;
}
Primjena operatora ->
kao u ovom slučaju
cout << spt1.get()->get() << endl;
vrlo je nepraktična, no preopterećenje operatora ->
će nam omogućiti da koristimo prirodnu sintaksu spt1->get()
,
kao da je spt1
pokazivač.
Napomena. U standardnoj biblioteci brojanje referenci je implementirano klasom shared_ptr<T>
.
std::shared_ptr<T>
std::shared_ptr<T>
je pametni pokazivač koji implementira brojanje referenci.
Koristimo ga kada želimo imati više pokazivača koji pokazuju na isti element i brinu o njegovom vijeku trajanja.
Definiran je u zaglavlju <memory>
.
Konstrukcija
-
Konstruktor uzima pokazivač čiji postaje vlasnik.
-
Dodijeljeni konstruktor kreira prazan objekt (pokazivač na objekt je
nullptr
). -
std::shared_ptr<T>
ima konstruktore kopije i operator pridruživanja koji implementiraju brojanje referenci. -
std::shared_ptr<T>
je moguće konstruirati preuzimanjem resursastd::unique_ptr<T>
objekta. -
Funkcija
std::make_shared<T>
kreira pametni pokazivač na zadanu vrijednost. Interno kreira objekt pomoću operatoranew
. Svoje argumente predaje konstruktoru objekta.
#include <memory>
#include <iostream>
int main()
{
std::shared_ptr<int> p1(new int(5));
std::unique_ptr<int> u1(new int(2));
std::shared_ptr<int> p2(std::move(u1) ); // inicijaliziraj shared pomoću
// unique
std::shared_ptr<int> p3{p1}; // CCtor
p2 = p3; // OP
std::cout << p1.use_count() << std::endl; // daje 3
std::cout << std::boolalpha << p1.unique() << std::endl; // false
std::cout << *p1 << " : " << *p2 << " : " << *p3 << std::endl;
std::cout << "adresa = " << p1.get() << std::endl;
// Ako imao samo vrijednost koju želimo pohraniti u
// shared_ptr koristimo make_shared.
std::shared_ptr<int> p4 = std::make_shared<int>(11);
std::cout << *p4 << std::endl;
std::cout << "adresa = " << p4.get() << std::endl;
return 0;
}
Neke od metoda klase std::shared_ptr<T>
su iste kao i kod klase std::unique_ptr<T>
:
Poziv | Značenje |
---|---|
|
Vraća pokazivač koji |
|
Vraća |
|
Vraća broj referenci, odnosno broj |
|
Odriče se vlasništva nad objektom. |
|
Objekt kojeg je do sada držao zamijenjuje s |
-
std::shared_ptr<T>
drži pokazivač na dinamički alociran objekt. Pogrešno mu je dati adresu objekta koji nije dinamički alociran! Ako imamo samo objekt, a želimo ga kontrolirati pomoćustd::shared_ptr<T>
objekta, treba koristiti funkcijustd::make_shared<T>
(iz zaglavlja<memory>
). -
Destruktor klase
std::shared_ptr<T>
poziva operatirdelete
na pokazivaču koji drži. -
Metoda
release()
ne postoji.
std::shared_ptr<T> i polja
Ako pokazivač na polje želimo čuvati u std::shared_ptr<T>
objekt, tada pri konstrukciji trebamo zadati
deleter.
std::shared_ptr<int> spv(new int[10], [](int *p) { delete [] p; });
for(std::size_t i=0; i<10; ++i)
spv.get()[i] = i;
-
Deleter je funkcija koja uzima pokazivač na tip i ne vraća ništa.
-
Za razliku od
std::unique_ptr<T>
ne možemo pisatistd::shared_ptr<T[]>
već moramo zadati deleter eksplicitno. Sučelje klase se ne mijenja i nemamo operatora indeksiranja.
std::shared_ptr<T> i cirkularna zavisnost
Brojanje referenci uvodi mogućnost cirkularne zavisnosti.
-
Objekt
A
drži pokazivač na objektB
dok objektB
drži pokazivač na objektA
. U tom slučaju nikad neće doći do dealokacije memorije.
Primjer. Klasa koja drži pametne pokazivače:
class Employee{
public:
std::string name;
std::shared_ptr<Employee> head;
std::vector<std::shared_ptr<Employee>> staff;
Employee(std::string const & name_,
std::shared_ptr<Employee> head_ = nullptr) : name(name_),
head(head_)
{}
~Employee(){ std::cout << name << " izbrisan\n"; }
// nema potrebe za ručnom dealokacijom
// ...
};
Funkcija za kreiranje instance klase:
std::shared_ptr<Employee> init(std::string const & name){
std::shared_ptr<Employee> boss(new Employee(name + " boss"));
std::shared_ptr<Employee> dept_boss(new Employee(name, boss));
boss->staff.push_back(dept_boss);
return dept_boss;
}
U glavnom programu …
int main()
{
// ...
std::shared_ptr<Employee> dboss = init("Damir");
std::cout << dboss->name << " use_count = " << dboss.use_count() << std::endl;
// Do dealokacije neće nikad doći!
return 0;
}
Da ne dolazi do dealokacije možemo provjeriti pomoću valgrind-a
valgrind --tool=memcheck --leak-check=yes ./a.out
std::weak_ptr<T>
Klasa weak_ptr<T>
omogućava dijeljenje dinamički alociranog objekta bez vlasništva nad objektom.
-
weak_ptr<T>
najčešće kreiramo izshared_ptr<T>
objekta. Oni tada pokazuju na isti objekt. Dodijeljeni konstruktor kreira prazan pokazivač (onaj koji držinullptr
). -
Metoda
use_count()
nikad ne brojiweak_ptr<T>
objekte koji drže pokazivač na objekt; ona broji samoshared_ptr<T>
objekte. -
weak_ptr<T>
nikad ne dealocira svoj objekt.
std::shared_ptr<int> sp1(new int(7));
std::shared_ptr<int> sp2(sp1);
std::weak_ptr<int> wp1(sp2);
std::cout << wp1.use_count() << std::endl; // 2
if(wp1.expired())
std::cout << "wp1 drži nullptr.\n";
else
std::cout << "wp1 pokazuje na objekt.\n";
Klasa weak_ptr<T>
ima minimalno sučelje i ne može direktno dohvatiti objekt na koji pokazuje
(niti za čitanje niti za pisanje).
Da bi mogla dohvatiti objekt prvo mora kreirati std::shared_ptr<T>
pomoću lock()
metode.
Poziv | Značenje |
---|---|
|
Vraća broj referenci, odnosno broj |
|
Vraća |
|
Vraća |
Na primjer (nastavak),
auto a =wp1.lock();
*a = 17;
std::cout << wp1.use_count() << std::endl; // 3
std::cout << *wp1.lock() << std::endl; // 17
weak_ptr<T>
je namijenjen i za korištenje u situacijama kada nadživi objekt na koji pokazuje.
Stoga prije uptrebe metode lock()
treba uvijek ispitati objekt sa expired()
metodom.
std::weakptr<T> i rješenje cirkularne zavisnosti
Korištenjem klase std::weak_ptr<Employee>
prekida se cirkularna zavisnost i
objekti se normalno dealociraju.
// std::weak_ptr<Employee> iprekida cirkularnu zavisnost.
class Employee{
public:
std::string name;
std::shared_ptr<Employee> head;
//std::vector<std::shared_ptr<Employee>> staff;
std::vector<std::weak_ptr<Employee>> staff;
Employee(std::string const & name_,
std::shared_ptr<Employee> head_ = nullptr) : name(name_),
head(head_)
{}
~Employee(){ std::cout << name << " izbrisan\n"; }
// nema potrebe za ručnom dealokacijom
};
Alokatori
Svi spremnici imaju jedan dodatni parametar predloška, tzv. alokator. Na primjer,
std::vector
ima deklaraciju:
template<
class T,
class Allocator = std::allocator<T>
> class vector;
Alokator brine o alokaciji i dealokaciji memorije. Operatori new
i delete
se ne koriste
neposredno zbog ovog "nedostatka":
-
Operator
new
kombinira alokaciju memorije i konstrukciju objekta u njoj. -
Operator
delete
kombinira destrukciju objekta i dealokaciju memorije,
Ta svojstva ovih operatora bi dovela do gubitka efikasnosti kod klasa kao što je std::vector<T>
koje alociraju više memorije no što
drže elemenata. Bez alokatora sva bi memorija morala biti konstruirana (s dodijeljenim konstruktorom).
Alokator iz zaglavlja <memory>
nam omogućava da razdvojimo fazu alokacije
memorije od konstrukcije objekta u memoriji te, isto tako fazu destrukcije objekta i dealokaciju
memorije. Dodijeljeni alokator iz standardne biblioteke je std::allocator<T>
. Korisnici mogu
pisati vlastite alokatore ako za tim imaju potrebe.
Ovdje su prikazane metode alokatora:
Poziv | Značenje |
---|---|
|
Definira alokator |
|
Alocira neinicijaliziranu memoriju za |
|
Dealocira memoriju koja sadrži prostor za |
|
|
|
|
Korisne mogu biti još dvije metode iz memory
zaglavlja;
Poziv | Značenje |
---|---|
|
Kopira elemente iz raspona danog s iteratorima |
|
Konstruira objekte u neinicijaliziranoj memoriji omeđenoj s iteratorima |
Postoje i copy_n i fill_n verzije tih funkcija koje uzimaju polazni iterator i broj elemenata.