Despre Smart Pointers - partea I
In aceasta lectie incepem discutia despre "Smart Pointers".
Ca sa puteti urmari aceasta lectie aveti nevoie de cunostinte de baza de C++ precum: supraincarcarea operatorilor si cateva notiuni elementare despre sabloane (templates). Urmatoarele lectii vor detalia ceva mai mult subiectul sabloanelor.
Numele variabilelor, functiilor, claselor, comentariile, .. etc sunt in limba engleza. Nu obisnuiesc sa folosesc limba romana in codul pe care il scriu, plus ca intentionez sa ca in viitor sa postez bucati din codul pe care l-am scris in decursul vremii si in care se foloseste doar limba engleza.
Inainte sa incepem discutia propiu-zisa, sa vorbim despre unul din bug-urile de programare care poate afecta proiectele scrise in C++, si anume
"memory leaks".
Limbajele de programare ce dispun de un
"garbage collector" reduc drastic posibilitatea de aparitie a astfel de bug-uri, pentru ca gestionarea memoriei este facuta de catre
"garbage collector".Ce inseamna un memory leak: este o eroare de programare in care programatorul a alocat memorie pe heap (utilizand functiile din libraria standard C (malloc, calloc, realloc) sau operatorul C++ new), dar
nu a mai dezalocat memoria alocata .
De ce este asta o problema: pentru ca consumul de memorie al aplicatiei poate creste in mod necontrolat. Problema devine critica mai ales in cazul aplicatiilor care trebuie sa ruleze un timp indelungat (de exemplu un server de baza de date, sau un server web, un filesystem ... etc).
Cum ne putem asigura ca o astfel de problema nu apare in cadrul unui proiect C++ ?
Sunt 2 posibilitati:A)
Management-ul memoriei sa fie facut manual.
Suntem atenti ca intodeauna sa dezalocam memoria pe care am alocat-o cand nu mai e nevoie de ea.
Problema acestei metode este ca e predispusa la erori. Pe masura ce complexitatea aplicatiei creste, e din ce in ce mai greu sa folosim aceasta metoda, pentru ca pe masura ce aplicatia devine mai complexa este din ce in ce mai greu sa urmaresti durata de viata a unui obiect.
Durata de viata (life-time) a obiectelor intr-o aplicatie C++ mare este un subiect complex, mai ales cand aplicatia foloseste mai multe thread-uri de executie si acele obiecte sunt accesate de pe mai multe thread-uri. Daca adaugam si exceptiile in aceasta ecuatie, atunci subiectul devine si mai complicat (vom vede imediat de ce)
B)
Management-ul memoriei sa se faca automat folosind smart-pointers . Aici exista mai multe tehnici care trebuiesc stapanite (fiecare abordand probleme diferite):
-
managementul obiectelor alocate dinamic folosind "scoped smart pointers" (SP la nivel de scop). Acest subiect va fi discutat in cadrul lectiei de fata
-
managementul obiectelor alocate dinamic folosind "smart pointers" si contorizarea de referinte (reference counting) (intrusiva si non-intrusiva) - eficientizarea consumului de memoriei prin reducerea copierii inutile a obiectelor alocate dinamic prin folosirea tehnicii
"copiere la scriere" (
"copy on write" - cunoscuta drept si COW de catre programatorii C++)
Ultimele 2 subiecte for fi abordate in lectiile viitoareBun, urmatoarea etapa este sa vedem diverse scenarii in care apar memory leaks:
a)
void f()
{
MyObject* p = new MyObject;
...... // many code lines
if (condition)
{
...... // many code lines
delete p;
}
else
{
...... // many code lines
}
......... // many code lines
}
Bug - daca conditia "condition" este falsa, atunci zona de memorie spre care indica pointer-ul "p" nu a fost dezalocata
b)
int f()
{
MyObject* p = new MyObject;
...... // many code lines
if (condition)
{
...... // many code lines
return (-1)
}
delete p;
}
Bug - daca conditia "condition" este adevarata, atunci iesim din functie fara sa dezalocam zona de memorie spre care indica p.
Ca sa reparam aceasta problema, ar trebui sa scriem
int f()
{
MyObject* p = new MyObject;
...... // many code lines
if (condition)
{
...... // many code lines
delete p; // don't forget to deallocate before every exit point from the function
return (-1)
}
delete p;
}
Adica trebuie sa ne asiguram ca nu exista nici o posibilitate sa face return din aceasta functie fara ca inainte de asta sa dezalocam zona de memorie spre care indica p.
Pare ceva simplu si de bun simt (si chiar este), dar e oarecum predispus la erori. Ganditi-va ca avem o functie complexa care are poate zeci de puncte de return si in care se aloca memorie de zeci de ori imprastiat in tot corpul functiei.
Inainte sa ajungem la un return din functie trebuie sa ne fie clar ce s-a alocat pana in momentul respectiv si sa dezalocam ... Plus ca daca avem 2 pointeri care indica catre aceeasi zona de memorie trebuie sa stim asta si sa apelam delete numai pe unul dintre ei ...
Asta va face ca codul respectivei functii sa creasca si mai mult (doar ca ne asiguram ca facem clean-up in mod corect).
Nu zic ca asa ar trebui sa scriem functiile, evident ca o functie complicata ar trebui sa fie sparta in mai multe functii mici, etc. Dar nu traim intr-o lume ideala, si in codul aplicatiilor mari vezi de multe ori exemple de functii imense si greu de urmarit ...
c)
void f()
{
MyObject* p = new MyObject;
...... // many code lines
if (condition)
{
...... // many code lines
delete p;
}
else
{
...... // many code lines
}
......... // many code lines
delete p;
}
Bug - daca conditia "condition" este adevarata, zona de memorie spre care indica pointer-ul "p" a fost dezalocata de 2 ori (genul acesta de eroare se numeste "double free"). Rezultatul acestei erori este un comportament imprevizibil al aplicatiei
d)
void f()
{
MyObject* p = new MyObject;
...
my_func(); // may throw exceptions
...
delete p;
...
}
Bug - daca functia my_func() arunca o exceptie, zona de memorie spre care indica pointer-ul "p" nu este dezalocata
Desi aceste situatii par simple, inchipuiti-va ca aveti functii complicate, care apeleaza alte zeci de functii care pot sau nu sa arunce exceptii sau ca pot exista multe cai de iesire din functie.
In momentul respectiv este posibil sa apara erori de genul "memory leaks" sau "double free"
OK, sa discutam cu ce ne poate ajuta pe noi notiunea de "smart pointer" in situatii de genul acesta.
Strategia este urmatoarea: sa "imachetam" pointer-ul "p" intr-un obiect pentru care pointer-ul respectiv este dezalocat automat in destructorul obiectului.
Ne bazam pe urmatoarea facilitate a limbajului C++:
- destructorul unui obiect alocat pe stiva este intodeauna apelat inainte ca functia pe frame-ul careia a fost alocat obiectul sa se termine (pe epilogul functiei). Compilatorul insereaza cod care sa se asigure ca destructorul este chemat. Destructorii obiectelor alocate pe stiva se apeleaza chiar si daca functia a aruncat o exceptie necaptata in functia respectivaDe exemplu:
void func()
{
MyObject obj;
.......
}
Obiectul obj este alocat pe stiva functiei func().
Destructorul sau este chemat automat chiar inainte ca functia func() sa se termine.In momentul de fata trebuie sa punem la punct limbajul pe care il vom folosi in continuare:
- ne vom referi la un pointer catre un obiect ("p" in exemplele anterioare) ca fiind un
"raw pointer"- ne vom referi la obiectul care impacheteaza pointer-ul "p" ca fiind un
"smart pointer"In momentul de fata incepem design-ul clasei "smart pointer".
Decizii de design:1. Smart pointer-ul ar trebui sa fie folosit la fel ca si un raw pointer (folosind aceeasi operatori).Sa ne gandim cum putem sa folosim un raw pointer:
struct MyObject
{
int a;
int b;
}
void func()
{
MyObject* p = new MyObject;
p->a = 1;
(*p).b = 2;
........
}
In acest caz ar trebui sa putem scrie ceva de genu:
sp->a = 1;
(*sp).b = 2;
unde sp este un obiect "smart pointer" care impacheteaza pointer-ul raw "p". Intentia este ca programatorul sa foloseasca cat mai usor "smart pointer-ul" (practic sa nici nu il intereseze ca utilizeaza un smart pointer sau un pointer raw.
Decizia de design 1) poate fi implementata usor folosind supraincarcarea operatorilor pentru clasa "smart pointer"
2. Smart pointer-ul sa fie flexibil Adica
politica de distrugere sa fie destul de flexibila sa distruga obiecte alocate in diverse moduri.
Ar trebui sa fie destul de flexibil ca sa poata fie folosit si pentru array-uri alocate dinamic.
In cate feluri pot fi alocate dinamic obiectele:
- Pot fi alocate folosind operatorul new
int* p = new int;
delete p;
...
int* q = new int[10];
delete [] q;
- Pot fi alocate folosind functii din libraria standard C (malloc, calloc, realloc):
...
int* p = (int *)malloc(sizeof(int));
free(p);
...
int* q = (int *)malloc(10 * sizeof(int));
free(q);
- Pot fi alocate folosind "placement new" (foarte utilizat in cazul in care vrem sa facem un allocator de memorie "custom", de exemplu pentru a aloca memorie dintr-un "pool" de memorie)
Ceea ce se observa din aceste exemple este ca dezalocarea obiectelor se face in mod diferit, in functie de cum au fost ele create. Noi vrem ca distrugerea memoriei indicate de catre pointer-ul raw impachetat "smart pointer" sa se imtample in momentul distrugerii obiectului "smart pointer", dar trebuie sa luam in calcul modalitatea in care pointer-ul raw a fost creat ca sa stim cum il dezalocam corect.
Decizia de design 2) poate fi implementata usor folosind o tehnica inventata de catre Andrei Alexandrescu (detaliata in cartea sa "Modern C++ design"). Tehnica sa numeste
"policy base design", si este folosita de multe librarii moderne scrise in C++, precum "boost" si "Loki".
In mod evident, clasa "smart pointer" trebuie sa poata fi utilizata pentru pointeri catre orice tip. Rezulta ca respectiva clasa va fi un sablon (clasa template). Clasa trebuie sa functioneze folosind o politica flexibila de distrugere a obiectelor (precum am discutat ceva mai devreme).
Clasa smart pointer pe care o dezvlotam in aceasta lectie NU poate fi utilizata in containere STL (se va obtine o eroare la compilare daca se incearca acest lucru) (si nici nu prea are sens sa fie folosita asa).
In lectiile viitoare vom implementa smart pointers care functioneaza in colaborare cu mecanismul de contorizare a referintelor, care vor putea fi folositi in containere STL.Bun, acum prezint codul.
Sunt 4 fisiere: - config_utils.h -> un header ce contine macro-uri utilitare (nimic interesant aici, il folosesc deoarece vreau ca codul sa poata fi folosit si pe compilatoare care nu accepta namespace-uri)
-
scoped_smart_ptr.h -> clasa pentru "smart pointer".
-
object_deletion_policies.h -> diverse clase template care parametrizeaza politica de distrugere a obiectelor
-
main.cpp -> un program de test
// config_utils.h
#ifndef _CONFIG_UTILS_H_
#define _CONFIG_UTILS_H_
#ifdef CW_HAS_NAMESPACES
#define CW_UTILS_NAMESPACE_NAME cw_utils
#define CW_UTILS_NAMESPACE_START namespace CW_UTILS_NAMESPACE_NAME {
#define CW_UTILS_NAMESPACE_END }
#else
#define CW_UTILS_NAMESPACE_NAME
#define CW_UTILS_NAMESPACE_START
#define CW_UTILS_NAMESPACE_END
#endif // CW_HAS_NAMESPACES
#endif // _CONFIG_UTILS_H_
Daca compilatorul vostru suporta namespace-uri, trebuie ca macro-ul CW_HAS_NAMESPACES sa fie definit inainte de includerea header-ului config_utils.h
#ifndef _PLUGIN_SCOPED_SMART_PTR_H_
#define _PLUGIN_SCOPED_SMART_PTR_H_
#include <cassert>
#include "object_deletion_policies.h"
#include "config_utils.h"
CW_UTILS_NAMESPACE_START
// smart pointer for NON reference counted heap allocated objects
// the object kept by the scoped smart pointer gets deallocated in the smart pointer destructor
// objects of type scoped_smart_ptr can be created ONLY on stack
template <class T, class object_deletion_policy>
class scoped_smart_ptr : private object_deletion_policy
{
typedef T object_type;
typedef T* object_type_pointer;
typedef T& object_type_reference;
private:
// copy construction disallowed
scoped_smart_ptr(const scoped_smart_ptr&);
object_type_pointer operator=(const scoped_smart_ptr&);
object_type_pointer operator=(object_type_pointer);
// disallow creating scoped_smart_ptr objects elsewhere than on the stack
// for simple objects
void* operator new(size_t);
// for arrays
void* operator new[](size_t);
// disallow delete since the objects will be created on stack only
void operator delete(void*);
void operator delete[](void*);
public:
scoped_smart_ptr(object_type_pointer lp = NULL) throw() : object_pointer(lp) {}
~scoped_smart_ptr() throw()
{
// don't propagate exceptions outside the destructor
try
{
if (object_pointer != NULL)
{
object_deletion_policy::dispose(object_pointer);
}
}
catch (...)
{
assert(0);
}
}
object_type_pointer get() const throw() {return object_pointer;}
operator object_type_pointer() const throw() {return object_pointer;}
void attach(object_type_pointer lp = NULL) throw()
{
if (object_pointer != NULL)
{
object_deletion_policy::dispose(object_pointer);
}
object_pointer = lp;
}
object_type_pointer detach() throw()
{
object_type_pointer old = object_pointer;
object_pointer = NULL;
return old;
}
object_type_reference operator*() throw() {assert(object_pointer != NULL); return (*object_pointer);}
object_type_pointer* operator&() throw() {return (&object_pointer);}
object_type_pointer operator->() const throw() {assert(object_pointer != NULL); return object_pointer;}
bool operator==(object_type_pointer lp) const throw() {return object_pointer == lp;}
bool operator!=(object_type_pointer lp) const throw() {return object_pointer != lp;}
bool operator!() throw() {return (object_pointer == NULL);}
void swap(scoped_smart_ptr& lp) throw()
{
T* tmp = lp.object_pointer;
lp. object_pointer = object_pointer;
object_pointer = tmp;
}
private:
object_type_pointer object_pointer;
};
// swap through a non member function
template<class T, class object_deletion_policy>
void swap(scoped_smart_ptr<T, object_deletion_policy>& a,
scoped_smart_ptr<T, object_deletion_policy>& b) throw()
{
a.swap(b);
}
template<class T>
struct standard_scoped_ptr
{
typedef scoped_smart_ptr<T, std_object_deletion_policy>type;
};
template<class T>
struct standard_verified_scoped_ptr
{
typedef scoped_smart_ptr<T, std_object_verified_deletion_policy>type;
};
template<class T>
struct standard_scoped_array_ptr
{
typedef scoped_smart_ptr<T, std_array_deletion_policy>type;
};
template<class T>
struct standard_verified_scoped_array_ptr
{
typedef scoped_smart_ptr<T, std_array_verified_deletion_policy>type;
};
template<class T>
struct c_standard_scoped_ptr
{
typedef scoped_smart_ptr<T, c_std_object_deletion_policy>type;
};
template<class T>
struct c_standard_verified_scoped_ptr
{
typedef scoped_smart_ptr<T, c_std_object_verified_deletion_policy>type;
};
template<class T>
struct c_standard_scoped_array_ptr
{
typedef scoped_smart_ptr<T, c_std_array_deletion_policy>type;
};
template<class T>
struct c_standard_verified_scoped_array_ptr
{
typedef scoped_smart_ptr<T, c_std_array_verified_deletion_policy>type;
};
CW_UTILS_NAMESPACE_END
#endif //_PLUGIN_SCOPED_SMART_PTR_H_
Ok, sunt multe de discutat aici. Clasa scoped_smart_ptr este clasa "smart pointer".
Ea este o clasa sablon, parametrizata dupa:
- T -> asta este tipul pointer-ului raw impachetat
- object_deletion_policy -> o clasa sablon care parametrizeaza diverse politici de distrugere a obiectelor
Pointer-ul raw impachetat are tipul T si este tinut in membrul "object_pointer" al clasei "scoped_smart_ptr"
Vedeti apoi ca constructorul de copiere al clasei, precum si operatorul de asignare sunt declarati ca fiind privati si nu sunt implementati.
De ce ? Din cauza modelului de
"ownership". Vreau sa ma asigur ca, in mod implicit, un singur obiect de tipul scoped_smart_ptr controleaza pointer-ul raw impachetat.
Daca un programator care foloseste aceasta clasa in genul:
scoped_smart_ptr<.....> my_obj1;
....
scoped_smart_ptr<.....> my_obj2(my_obj1); // error, copy constructor is private
...
scoped_smart_ptr<.....> my_obj2 = my_obj1; // error, asignment operator is private
va obtine
erori la compilare.
In continuare, in destructor, vedeti ca nu distrugerea obiectului indicat de membrul object_pointer se face prin intermediul functiei dispose, membru al clasei object_deletion_policy.
Acesta este motivul pentru care am si
derivat clasa scoped_smart_ptr din clasa object_deletion_policy, ca sa putem apela functia dispose() din clasa de baza object_deletion_policy.
De asemenea, observati ca destructorul foloseste clauza "catch-all".
Nu vrem ca destructorul sa arunce exceptii, ar fi un bug de programare. Din acest motiv, functia dispose() membra a clase object_deletion_policy nu trebuie sa arunce exceptii.
- functia get() -> poate fi folosita pentru obtinerea pointer-ului raw impachetat. Nu se recomanda a se folosi decat in cazuri extreme, deoarece ocloleste mecanismul de "smart pointer" (in fond, de aia folosim smart pointers ca sa manevram pointeri raw prin intermediu lor, nu-i asa ?)
- object_type_pointer() -> supraincarc operatorul de cast sa intoarca pointer-ul raw impachetat. Nimic spectaculos aici.
- functia attach() -> ataseaza smart pointer-ul la un pointer raw. Daca smart pointer-ul impacheteaza deja un pointer raw, il distruge inainte de a imacheta noul pointer raw
- functia detach() -> detaseaza smart pointer-ul de la pointerul raw continut si intoarce pointer-ul raw
- operatorii "*", "->" si "&" sunt supraincarcati. Va amintiti ca in decizia de design 1) am spus ca un smart pointer ar trebui sa poate sa fie folosit exact ca si raw pointer-ul continut. Acesta este motivul pentru care acesti operatori sunt supraincarcati
- in continuare sunt supraincarcati operatorii de egalitate si de negare logica
- functia swap() -> interschimba pointerii raw impachetati intre 2 obiecte de tip smart pointer. NU trebuie sa arunce exceptii.
In continuare urmeaza niste clase sablon care sa faca mai usoara folosirea clasei scoped_smart_ptr.
standard_scoped_ptr -> smart pointer care impacheteaza pointeri catre obiecte singulare alocate folosind operatorul new.
Dezalocarea se face folosind operatorul delete
standard_verified_scoped_ptr -> smart pointer care impacheteaza pointeri catre obiecte singulare alocate folosind operatorul new. Dezalocarea se face folosind operatorul delete. In plus, fata de precedenta clasa, se face o verificare ca pointer-ul raw impachetat nu este NULL
standard_scoped_array_ptr -> smart pointer care impacheteaza pointeri catre array-uri alocate dinamic folosind new[]. Dezalocarea se face folosind delete[]
standard_verified_scoped_array_ptr -> smart pointer care impacheteaza pointeri catre array-uri alocate dinamic folosind new[]. Dezalocarea se face folosind delete[]. In plus, fata de precedenta clasa, se face o verificare ca pointer-ul raw impachetat nu este NULL
c_standard_scoped_ptr -> smart pointer care impacheteaza pointeri catre obiecte singulare alocate dinamic folosind una din functiile existente in libraria stadard C (malloc, calloc, realloc). Dezalocarea se face folosind functia free() (tot din libraria standard C)
c_standard_verified_scoped_ptr -> smart pointer care impacheteaza pointeri catre obiecte singulare alocate dinamic folosind una din functiile existente in libraria stadard C (malloc, calloc, realloc). Dezalocarea se face folosind functia free() (tot din libraria standard C). In plus, fata de precedenta clasa, se face o verificare ca pointer-ul raw impachetat nu este NULL
c_standard_scoped_array_ptr -> smart pointer care impacheteaza pointeri catre array-uri alocate dinamic folosind una din functiile existente in libraria stadard C (malloc, calloc, realloc). Dezalocarea se face folosind functia free() (tot din libraria standard C).
c_standard_verified_scoped_array_ptr -> smart pointer care impacheteaza pointeri catre array-uri alocate dinamic folosind una din functiile existente in libraria stadard C (malloc, calloc, realloc). Dezalocarea se face folosind functia free() (tot din libraria standard C). In plus, fata de precedenta clasa, se face o verificare ca pointer-ul raw impachetat nu este NULL
Trecem la fisierul
object_deletion_policies.h//object_deletion_policies.h
// policies for objects deletion - inspired by Andrei Alexandrescu's "Modern C++ programming"
#ifndef _PLUGIN_OBJECT_DELETION_POLICIES_H_
#define _PLUGIN_OBJECT_DELETION_POLICIES_H_
#include "config_utils.h"
#include <cstdlib> // for free
CW_UTILS_NAMESPACE_START
// Do nothing dispose operation
struct null_object_deletion_policy
{
template<class T>
inline void dispose (T* object_pointer) throw() {}
};
// standard C++ delete policy for single objects (allocated using the new operator)
struct std_object_deletion_policy
{
template<class T>
inline void dispose (T* object_pointer) throw() {delete object_pointer;}
};
// standard C++ delete policy for single objects with NULL verification
struct std_object_verified_deletion_policy
{
template<class T>
inline void dispose (T* object_pointer) throw()
{
if (object_pointer != NULL)
delete object_pointer;
}
};
// standard C++ delete policy for object array (allocated using new [])
struct std_array_deletion_policy
{
template<class T>
inline void dispose (T* array) throw() {delete [] array;}
};
// standard C++ delete policy for object array with NULL verification
struct std_array_verified_deletion_policy
{
template<class T>
inline void dispose (T* array) throw()
{
if (array != NULL)
delete [] array;
}
};
// delete policy for objects allocated with malloc
struct c_std_object_deletion_policy
{
template<class T>
inline void dispose (T* object_pointer) throw() {free(object_pointer);}
};
// delete policy for objects allocated with malloc with NULL verification
struct c_std_object_verified_deletion_policy
{
template<class T>
inline void dispose (T* object_pointer) throw()
{
if (object_pointer != NULL)
free(object_pointer);
}
};
typedef c_std_object_deletion_policy c_std_array_deletion_policy;
typedef c_std_object_verified_deletion_policy c_std_array_verified_deletion_policy;
CW_UTILS_NAMESPACE_END
#endif //_PLUGIN_OBJECT_DELETION_POLICIES_H_
Acest fisier contine cateva clase sablon care implementeaza diverse politici de dezalocare.
E indeajuns sa cititi comentariile pentru a vedea ce face fiecare clasa.Bun, am terminat cu implementarea. Sa vedem cum se foloseste smart pointer-ul in practica.
Sa zicem ca avem urmatoarea clasa:
class MyClass
{
public:
MyClass(int a = 0, int b = 0) : m_a(a), m_b(b) {}
int m_a, m_b;
~MyClass() {cout << "Destructor called m_a = " << m_a << " m_b = " << m_b << endl;}
};
Cum am folosi aceasta clasa in mod normal (folosind doar pointeri raw):
void func()
{
MyClass* p = new MyClass;
p->m_a = 1;
p->m_b = 2;
.....
delete p; // don't forget this, you'll get a memory leak
}
Cum putem folosi un smart pointer pentru a scapa de obligativitatea dezalocarii manuale:
void func()
{
standard_scoped_ptr<MyClass>::type p(new MyClass);
p->m_a = 1;
p->m_b = 2;
} // notice that de memory allocated to hold the MyClass was automatically deallocated in the destructor of stack
// object "p". The destructor of object "p" is automatically called when the function func() ends.
Precum vedeti, smart pointer-ul se foloseste exact ca si un raw pointer: p->m_a = 1; Asta multumita supraincarcarii operatorului de indirectare "->".
Bun, acum urmeaza codul de test (main.cpp). Este foarte util sa vedeti cu se utilizeaza smart pointer-ul in diverse situatii.
// main.cpp
#include <cstdlib>
#include <iostream>
#include <cstring>
#define CW_HAS_NAMESPACES
#include "config_utils.h"
#include "scoped_smart_ptr.h"
#ifdef CW_HAS_NAMESPACES
using namespace CW_UTILS_NAMESPACE_NAME;
using namespace std;
#endif
class MyClass
{
public:
MyClass(int a = 0, int b = 0) : m_a(a), m_b(b) {}
int m_a, m_b;
~MyClass() {cout << "Destructor called m_a = " << m_a << " m_b = " << m_b << endl;}
};
void func1()
{
cout << "func1()" << endl;
standard_scoped_ptr<MyClass>::type ptr;
// testing attach
ptr.attach(new MyClass);
// testing operator overloading
ptr->m_a = 5;
(*ptr).m_b = 7;
ptr.attach(new MyClass);
}
void func2()
{
cout << "func2()" << endl;
// testing SP for dynamically allocated arrays
standard_scoped_array_ptr<MyClass>::type ptr(new MyClass[5]);
for (int i = 0; i < 5; i++)
{
ptr[i].m_a = i;
ptr[i].m_b = 2*i;
}
}
void func3()
{
cout << "func3()" << endl;
standard_scoped_array_ptr<char>::type ptr;
ptr.attach(new char[10]);
strcpy(ptr, "Hello");
cout << ptr << endl;
ptr[0] = 'X';
cout << ptr << endl;
}
void func4()
{
cout << "func4()" << endl;
standard_scoped_ptr<MyClass>::type ptr1(new MyClass);
ptr1->m_a = 1;
ptr1->m_b = 2;
standard_scoped_ptr<MyClass>::type ptr2(new MyClass);
ptr2->m_a = 3;
ptr2->m_b = 4;
cout << ptr1->m_a << endl;
cout << ptr1->m_b << endl;
cout << ptr2->m_a << endl;
cout << ptr2->m_b << endl;
ptr1.swap(ptr2);
cout << ptr1->m_a << endl;
cout << ptr1->m_b << endl;
cout << ptr2->m_a << endl;
cout << ptr2->m_b << endl;
}
void func5()
{
cout << "func5()" << endl;
standard_scoped_ptr<MyClass>::type ptr1(new MyClass);
ptr1->m_a = 2;
ptr1->m_b = 4;
MyClass* p = ptr1.detach();
cout << "Deleting raw pointer" << endl;
delete p;
}
int main(int argc, char *argv[])
{
func1();
cout << endl << endl;
func2();
cout << endl << endl;
func3();
cout << endl << endl;
func4();
cout << endl << endl;
func5();
return EXIT_SUCCESS;
}
Am folosit Linux si g++ pentru acest tutorial.
Compilarea si rularea codului de test (din shell) se face astfel:
g++ -I. main.cpp -o test
./test
NOTA: O clasa sablon se poate declara si astfel:template <typename T>
my_class
{
.....
}
Acest lucru este perfect echivalent cu:template <class T>
my_class
{
.....
}
Reguli de programare cand folosim smart pointers:
1. Imediat dupa ce obtinem un pointer raw catre o zona de memorie alocata dinamic impachetam pointerul raw intr-un smart pointer
2. Din momentul in care avem un obiect smart pointer folosim doar acel obiect pentru a accesa obiectul indicat de catre raw pointer. NU trebuie folosit in acelasi timp si raw pointer-ul si smart pointer-ul
3. Un raw pointer este continut intr-un singur smart pointer, pentru a evita dezalocarea aceleiasi zone de memorie de mai multe ori.
Libraria boost implementeaza 2 clase similare pentru "scoped smart pointer":
boost::scoped_ptr si
boost::scoped_array. Totusi, aceste clase sunt mult mai simple si nu folosesc un design bazat pe policy-uri.
boost:scoped_ptr impacheteaza un pointer la un obiect alocat dinamic folosind operatorul new (si in concluzie il dezaloca folosind operatorul delete).
boost::scoped_array impacheteaza un pointer catre un array alocat dinamic folosind new[] (iar dezalocarea se face folosind delete[]).
Clasa pentru smart pointers descrisa in acest tutorial poate fi instantiata sa foloseasca diverse politici de dezalocare. Design-ul este
deschis, un programator poate sa isi
implementeze o clasa sablon similara celor din object_deletion_policies.h si sa implementeze o
politica de dezalocare specifica aplicatiei lui. Strategia este foarte buna in cazul cand o aplicatie foloseste pool-uri si alocatori custom peste pool-uri de memorie, situatie in care de obicei obiectele se construiesc dinamic folosind operatorul "placement new" iar dezalocarea NU se face apeland operatorii delete sau delete[], ci trebuie chemat manual destructorul obiectului in cauza. Toata aceasta complexitate se poate ascunde in spatele unei clase policy similare celor deja implementate in fisierul object_deletion_policies.h