Er bleek vraag naar te zijn, dus bij deze een eerste tutorial. Onderstaande tekst is vrij voor iedereen te gebruiken, maar het is wel zo vriendelijk en politiek correct als het echt nuttig voor je is een acknowledgement naar de auteur in je code te zetten (zie mn profile voor naam en email).
Btw. ik heb de codestukken hier half on-the-fly geschreven en half gekopieerd uit bestaande eigen code. De eersten zijn in Nederlands becommentariseerd en de tweede in het Engels (ben ik nu eenmaal vanuit m'n werk gewend).
Naming conventions
Even een woordje vooraf over de naming conventions die ik gebruik: ik walg namelijk van de Hongaarse notatie die bijna de hele industrie gebruikt. Deze houdt in dat je voor een variabelenaam een afkorting zet van het datatype, dus bijvoorbeeld een DWORD index wordt dan dwIndex. De reden dat ik het een waardeloos systeem vindt is dat ik bij een index echt wel kan raden dat het een of andere integer-variant is, en een buffer zal echt wel een Long Pointer zijn, dus lpBuffer is ook wat nutteloos. Wat veel nuttiger is imho is om te weten welke scope het kreng heeft. Daarom gebruik ik de volgende prefixes voor variabelen:
Tevens ben ik geen fan van datatypes als ALL CAPS, DAN LIJKT HET NET ALSOF DIE SOURCECODE TEGEN JE SCHREEUWT. Datatypes krijgen bij mij netjes een t_ prefix, en een enumerate een e_ prefix. Charmant systeem als je het gewend bent, en het bevordert zwaar de leesbaarheid van constructors en zo:
m_SomeVariable = p_SomeVariable;
Om de meest gebruikte datatypes binnen deze conventions te vangen maken we even een paar typedefs, het getalletje geeft het aantal gebruikte bytes en de S en U staan voor signed/unsigned:
Voordeel van deze aanpak is ook dat je bijvoorbeeld heel simpel het project Unicode kunt maken door de t_Char definitie te wijzigen naar wchar_t. Tevens definieren we onze eigen boolean, daar niet iedere compiler het echt nauw neemt met het ANSI-voorschrift dat TRUE tegenwoordig echt 1 moet zijn:
Framework Basics
Wat is een framework? Even de schoolboekjes erbij: A framework is a collection of classes that simplifies programming by encapsulating often-used programming techniques. Een hierarchie van klassen dus die vaak voorkomende programmeertaken simplificeert of overneemt. Het belangrijkste onderdeel van een framework is de zgn. root-class, waarvan alle andere klassen uiteindelijk afgeleid zijn. Traditie wil dat deze xxxObject heet: MFC heeft CObject, VCL heeft TObject, en ook wij gaan hier een Object gebruiken. We zullen hem maar MyObject noemen, en om bepaalde redenen wordt het voluit CMyObject (hier kom ik dadelijk op terug).
De root-class introduceert over het algemeen RTTI-gedrag, oftewel RunTime Type Info, waardoor de class feitelijk weet wat voor class hij is en dit kan vertellen. En we gaan er nog wat meer gedrag in stoppen, maar dat komt nu:
Smart pointers
Een van de grootste plagen van C++ voor zowel onervaren als ervaren coders is het gebrek aan de garbagecollector van Java of een andere interpreter: geheugen dat je vergeet vrij te geven is KWIJT tot de volgende reboot. Correct gedrag hierop zou zijn dat je tientallen try/catch constructies in mekaar draait, wat al vaak wordt gesimplificeerd tot 1 try/catch, maar nog steeds veel code-vervuiling levert. Aan de andere kant van het verhaal komt de eerdergenoemde garbagecollector die door Java en C# wordt gebruikt: die is ook allesbehalve heilig. Beide talen kennen destructors, maar geen kip gebruikt ze omdat de taalspecificaties simpelweg niet garanderen wanneer ze worden aangeroepen, of zelfs DAT ze ooit worden aangeroepen. Terwijl destructors zo nuttig zijn... dat moet toch beter kunnen?
Beide punten worden gecombineerd opgelost door een Smart Pointer systeem. Kort samengevat houdt het in dat we geen directe pointers naar een class hebben, maar dat er een kleine class op de stack of tussen de membervariables zit die VERWIJST naar de class, en zich verder als een pointer gedraagt. De class zelf weet hoe vaak er naar hem verwezen wordt, en vernietigt zichzelf zodra hij nergens meer bestaat, en de smart pointer registreert dit allemaal in de class. Nooit meer sterretjes zien in je code of memory leaks! Sterker nog: omdat we nu zelf de pointer schrijven krijgen we zelfs nooit meer de beroemde access violations of segmentation faults!
Smart pointer implementatie
Wat houdt dit in voor ons prille frameworkje? Hier komt kijken waarom we de onze root-object CMyObject gingen noemen: de smart pointer die erbij hoort gaan we namelijk RMyObject noemen (kort voor Reference to MyObject). De class zelf krijgt een C als prefix daar het een Complex class is. Basic classes, zoals strings en exceptions, hebben geen smart pointer nodig en krijgen daarom een B als prefix (BMyString bijvoorbeeld). Het feitelijke verschil is dat een Complex class dmv. new wordt aangemaakt, en dat Basic en Reference classes zich altijd op de stack of in een class bevinden.
Wat we nu willen is dat het volgende stukje code werkt zonder memory leaks, en dat de destructor van onze CMyObject automatisch wordt aangeroepen bij het verlaten van de functie.
Toegegeven, het ziet er uit als je reinste science-fiction, maar dit gaat werken!
De definitie van CMyObject
Ter simplificering zullen we op dit moment in CMyObject nog geen RTTI-informatie stoppen en ons enkel beperken tot de smart pointer functionaliteit, en we negeren voorlopig ook multithreading problemen. Wat hebben we nodig?
-Een reference counter zodat we weten hoe vaak we worden gebruikt.
-Een virtual destructor omdat dan automatisch alle afgeleide classes virtual destructors hebben.
-Een manier om het reference object of afgeleide klassen de counter te laten wijzigen.
Dit wordt dan voorlopig:
Deze definitie spreekt denk ik knap voor zichzelf, dus we gaan maar meteen door naar de implementatie:
Dit wijst allemaal ook voor zich, behalve wellicht de DecreaseReference: mag dat wel, jezelf deleten? Maar natuurlijk! Een memberfunctie zoals deze is gewoon een blokje code dat ergens staat, en dus geen fysiek onderdeel van de class. Dus de code bestaat gewoon nog na delete. Wel moeten we hier oppassen dat we niet m_ReferenceCount teruggeven want die bestaat op dat moment NIET meer... vandaar de return 0;.
De smart pointer implementeren
Okee dit is in vergelijking met het voorgaande een hel, dus zet je schrap... wat willen we?
-Hij moet verwijzen naar een CMyObject class
-Alle public functionaliteit van de referenced object moet beschikbaar zijn
-We moeten de pointer 100% als een C++ pointer kunnen gebruiken.
Hier komt ie, uitleg volgt:
Om de eerste vraag maar vast te stellen: waarom is er hier niets virtual?!? Botweg omdat het niet nodig is cq. niet kan... IsValid en IsInvalid zitten hier in hun echte final implementation, en de andere functies worden allemaal in afgeleide klassen opnieuw gedefinieerd met andere parameterlists (en zijn dus ook final)
Allereerst zien we 3 constructors: eentje die het ding gewoon instantieert, eentje die het resultaat van new CMyObject pikt, en een copy-constructor. Voordat ik op de p_Safe inga moet ik eerst bekennen dat je met Smart Pointers WEL memoryleaks kunt hebben... Stel dat je een class A heeft die een pointer naar class B heeft, en vice versa. Ze hebben allebei dankzij elkaar minimaal een referencecount van 1, en zullen dus nooit als eerste vernietigd worden, en ze blijven dus eeuwig bestaan! Deze circular references komen in een goed opgezet systeem alleen voor in hierarchieen, waarbij een node zowel zn parentnode als zn childnodes moet kennen. In deze situatie moeten de children altijd opgeruimd worden voor de node zelf, en moet de reference naar de parent dus unsafe zijn. Als de flag op false staat doet de node dus niets met de reference count van zn object, maar heb je nog steeds de andere voordelen zoals de toegangsbescherming. De implementaties:
De destructor doet simpelweg het omgekeerde: als we naar een object verwijzen, laten we dan object weten dat we hem niet meer nodig hebben:
Hierna volgen de IsValid en IsInvalid functies, welke simpelweg een Boolean teruggeven of de reference al of niet naar een geldige class wijzen (de implementatie kun je zelf wel bedenken
). Wellicht rijst nu wel de vraag hoe je bij deze functies moet komen als we ook nog de functies van de Complex class moeten kunnen bereiken, wat heel simpel is: gewoon pointergedrag! De -> operator wijst naar het object, en de . operator wijst naar de reference class zelf. Bijvoorbeeld:
De CanConvertFrom en ConvertFrom functies worden pas interessant in afgeleide classes, en er kan hier in de root-class simpelweg volstaan worden met het teruggeven van e_True op de eerste, en een assign uitvoeren op de 2e. In inherited classes gaat dit de automatische dynamic casting verzorgen.
De overloads van de = operator zorgen ervoor dat we andere pointers aan deze kunnen toewijzen. Er is voor zowel voor CMyObject als voor RMyObject een versie omdat we dan zelf de meest geoptimaliseerde versie kunnen geven (deze functie zal miljoenen keren worden aangeroepen dus het is de moeite waard). De volgende implementatie is wederom NIET threadsafe (omdat we botweg de benodigde objecten in ons framework nog niet hebben):
Je kunt goed zien hier hoe de Safe in het spelletje meedoet: unsafe laten we simpelweg alle referencecounts met rust. De comparison overloads vergelijken enkel het parameter object met het referenced object en retourneren aan de hand hiervan een boolean (vul zelf de implementatie maar in...). Ik heb expres geen overload voor const RMyObject & gedefinieerd, omdat dit met de volgende operator overloads door de compiler impliciet overbodig wordt gemaakt (als je echt wil optimaliseren kun je ze wel toevoegen). Deze overloads emuleren enkele standaardstukjes C++, zoals het dereferencen van een pointer en de address-of operator. Ze zijn als volgt geimplementeerd:
Zoals je ziet zijn het knap korte functies, en ze kunnen dus ook makkelijk als inline gedefinieerd worden.
Als laatste komt de ECHT meest gebruikte functie van de reference class: de overload van de -> operator zodat de reference class als een echte pointer functioneert. We checken eerst of er wel een geldig object is, en gooien een exceptie (BMyException noemen we m maar voorlopig, met een string als exception-tekst) indien het niet zo is. Anders geven we de pointer terug:
Simpel toch?
Conclusie
Een programma schrijven op basis van een smart-pointer systeem is vrijwel altijd een goed idee. De voordelen:
-NOOIT meer memoryleaks zolang je op je circular references let.
-NOOIT meer access violations of segmentation faults.
-Cleane makkelijk te lezen code.
-Macht in je destructors.
De nadelen:
-Je programma wordt er een paar procent langzamer door, zon 1-5% afhankelijk van het gebruik en de CPU.
-Let op je circular references
U wilt meer?
Ja, we willen bijvoorbeeld een class afleiden van CMyObject
Dit is de volgende stunt op het programma, en gaat weer een stevige lap tekst worden. Tenzij ik een stapel positieve reacties op deze eerste tutorial krijg gaat het vervolg er dan ook niet komen (dan heb ik ook wel iets beters te doen
)
Voor vragen, gebruik onderstaande ruimte:
Btw. ik heb de codestukken hier half on-the-fly geschreven en half gekopieerd uit bestaande eigen code. De eersten zijn in Nederlands becommentariseerd en de tweede in het Engels (ben ik nu eenmaal vanuit m'n werk gewend).
Naming conventions
Even een woordje vooraf over de naming conventions die ik gebruik: ik walg namelijk van de Hongaarse notatie die bijna de hele industrie gebruikt. Deze houdt in dat je voor een variabelenaam een afkorting zet van het datatype, dus bijvoorbeeld een DWORD index wordt dan dwIndex. De reden dat ik het een waardeloos systeem vindt is dat ik bij een index echt wel kan raden dat het een of andere integer-variant is, en een buffer zal echt wel een Long Pointer zijn, dus lpBuffer is ook wat nutteloos. Wat veel nuttiger is imho is om te weten welke scope het kreng heeft. Daarom gebruik ik de volgende prefixes voor variabelen:
code:
1
2
3
4
5
| l_ Local variable m_ Member variable c_ Constant variable p_ Parameter g_ Global variable |
Tevens ben ik geen fan van datatypes als ALL CAPS, DAN LIJKT HET NET ALSOF DIE SOURCECODE TEGEN JE SCHREEUWT. Datatypes krijgen bij mij netjes een t_ prefix, en een enumerate een e_ prefix. Charmant systeem als je het gewend bent, en het bevordert zwaar de leesbaarheid van constructors en zo:
m_SomeVariable = p_SomeVariable;
Om de meest gebruikte datatypes binnen deze conventions te vangen maken we even een paar typedefs, het getalletje geeft het aantal gebruikte bytes en de S en U staan voor signed/unsigned:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Atomic types typedef unsigned char t_UInt1; typedef unsigned short t_UInt2; typedef unsigned long t_UInt4; typedef unsigned __int64 t_UInt8; typedef char t_SInt1; typedef short t_SInt2; typedef long t_SInt4; typedef __int64 t_SInt8; typedef float t_Float4; typedef double t_Float8; typedef void* t_Pointer; typedef char t_Char; typedef unsigned char* t_Data; |
Voordeel van deze aanpak is ook dat je bijvoorbeeld heel simpel het project Unicode kunt maken door de t_Char definitie te wijzigen naar wchar_t. Tevens definieren we onze eigen boolean, daar niet iedere compiler het echt nauw neemt met het ANSI-voorschrift dat TRUE tegenwoordig echt 1 moet zijn:
code:
1
2
3
4
5
| typedef enum
{
e_False = 0,
e_True = 1
} t_Bool; |
Framework Basics
Wat is een framework? Even de schoolboekjes erbij: A framework is a collection of classes that simplifies programming by encapsulating often-used programming techniques. Een hierarchie van klassen dus die vaak voorkomende programmeertaken simplificeert of overneemt. Het belangrijkste onderdeel van een framework is de zgn. root-class, waarvan alle andere klassen uiteindelijk afgeleid zijn. Traditie wil dat deze xxxObject heet: MFC heeft CObject, VCL heeft TObject, en ook wij gaan hier een Object gebruiken. We zullen hem maar MyObject noemen, en om bepaalde redenen wordt het voluit CMyObject (hier kom ik dadelijk op terug).
De root-class introduceert over het algemeen RTTI-gedrag, oftewel RunTime Type Info, waardoor de class feitelijk weet wat voor class hij is en dit kan vertellen. En we gaan er nog wat meer gedrag in stoppen, maar dat komt nu:
Smart pointers
Een van de grootste plagen van C++ voor zowel onervaren als ervaren coders is het gebrek aan de garbagecollector van Java of een andere interpreter: geheugen dat je vergeet vrij te geven is KWIJT tot de volgende reboot. Correct gedrag hierop zou zijn dat je tientallen try/catch constructies in mekaar draait, wat al vaak wordt gesimplificeerd tot 1 try/catch, maar nog steeds veel code-vervuiling levert. Aan de andere kant van het verhaal komt de eerdergenoemde garbagecollector die door Java en C# wordt gebruikt: die is ook allesbehalve heilig. Beide talen kennen destructors, maar geen kip gebruikt ze omdat de taalspecificaties simpelweg niet garanderen wanneer ze worden aangeroepen, of zelfs DAT ze ooit worden aangeroepen. Terwijl destructors zo nuttig zijn... dat moet toch beter kunnen?
Beide punten worden gecombineerd opgelost door een Smart Pointer systeem. Kort samengevat houdt het in dat we geen directe pointers naar een class hebben, maar dat er een kleine class op de stack of tussen de membervariables zit die VERWIJST naar de class, en zich verder als een pointer gedraagt. De class zelf weet hoe vaak er naar hem verwezen wordt, en vernietigt zichzelf zodra hij nergens meer bestaat, en de smart pointer registreert dit allemaal in de class. Nooit meer sterretjes zien in je code of memory leaks! Sterker nog: omdat we nu zelf de pointer schrijven krijgen we zelfs nooit meer de beroemde access violations of segmentation faults!
Smart pointer implementatie
Wat houdt dit in voor ons prille frameworkje? Hier komt kijken waarom we de onze root-object CMyObject gingen noemen: de smart pointer die erbij hoort gaan we namelijk RMyObject noemen (kort voor Reference to MyObject). De class zelf krijgt een C als prefix daar het een Complex class is. Basic classes, zoals strings en exceptions, hebben geen smart pointer nodig en krijgen daarom een B als prefix (BMyString bijvoorbeeld). Het feitelijke verschil is dat een Complex class dmv. new wordt aangemaakt, en dat Basic en Reference classes zich altijd op de stack of in een class bevinden.
Wat we nu willen is dat het volgende stukje code werkt zonder memory leaks, en dat de destructor van onze CMyObject automatisch wordt aangeroepen bij het verlaten van de functie.
code:
1
2
3
4
5
6
7
8
9
| void Main()
{
RMyObject l_FirstObject = new CMyObject;
RMyObject l_SecondObject = l_FirstObject;
t_UInt4 l_ReferenceCount;
// Even vragen of we echt 2 keer gebruikt worden
l_ReferenceCount = l_SecondObject->ReferenceCount();
} |
Toegegeven, het ziet er uit als je reinste science-fiction, maar dit gaat werken!
De definitie van CMyObject
Ter simplificering zullen we op dit moment in CMyObject nog geen RTTI-informatie stoppen en ons enkel beperken tot de smart pointer functionaliteit, en we negeren voorlopig ook multithreading problemen. Wat hebben we nodig?
-Een reference counter zodat we weten hoe vaak we worden gebruikt.
-Een virtual destructor omdat dan automatisch alle afgeleide classes virtual destructors hebben.
-Een manier om het reference object of afgeleide klassen de counter te laten wijzigen.
Dit wordt dan voorlopig:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class CMyObject
{
public: // Methods
CMyObject();
virtual ~CMyObject();
inline const t_UInt4& ReferenceCount() const { return m_ReferenceCount; }
protected: // Methods
t_UInt4 IncreaseReference();
t_UInt4 DecreaseReference();
private: // Properties
t_UInt4 m_ReferenceCount;
friend class RMyObject;
}; |
Deze definitie spreekt denk ik knap voor zichzelf, dus we gaan maar meteen door naar de implementatie:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| CMyObject::CMyObject()
{
// Reset de reference counter omdat niemand nog naar ons verwijst
m_ReferenceCount = 0;
}
CMyObject::~CMyObject()
{
// Lege destructor, puur en alleen voor de virtualiteit
}
t_UInt4 CMyObject::IncreaseReference()
{
// TODO: Dit moet nog een keertje threadsafe worden...
return ++m_ReferenceCount;
}
t_UInt4 CMyObject::DecreaseReference()
{
// Check of de reference count nu nul wordt
if(--m_ReferenceCount == 0)
{
// Delete onszelf en return de nul counter
delete this;
return 0;
}
return m_ReferenceCount;
} |
Dit wijst allemaal ook voor zich, behalve wellicht de DecreaseReference: mag dat wel, jezelf deleten? Maar natuurlijk! Een memberfunctie zoals deze is gewoon een blokje code dat ergens staat, en dus geen fysiek onderdeel van de class. Dus de code bestaat gewoon nog na delete. Wel moeten we hier oppassen dat we niet m_ReferenceCount teruggeven want die bestaat op dat moment NIET meer... vandaar de return 0;.
De smart pointer implementeren
Okee dit is in vergelijking met het voorgaande een hel, dus zet je schrap... wat willen we?
-Hij moet verwijzen naar een CMyObject class
-Alle public functionaliteit van de referenced object moet beschikbaar zijn
-We moeten de pointer 100% als een C++ pointer kunnen gebruiken.
Hier komt ie, uitleg volgt:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| class RMyObject
{
public: // Methods
RMyObject(t_Bool p_Safe = e_True);
RMyObject(CMyObject* p_Object);
RMyObject(const RMyObject &p_Reference);
virtual ~RMyObject();
// Null pointer checks
inline t_Bool IsValid() const;
inline t_Bool IsInvalid() const;
// Class conversion functions
t_Bool CanConvertFrom(CMyObject* p_Object);
void ConvertFrom(CMyObject* p_Object);
// Assignment operators
void operator=(CMyObject *p_Object);
void operator=(const RMyObject &p_Reference);
// Comparison operators
inline t_Bool operator==(CMyObject *p_Object) const;
inline t_Bool operator!=(CMyObject *p_Object) const;
// Basic operator overloads
operator CMyObject*() const;
CMyObject& operator*() const;
CMyObject* operator&() const;
CMyObject* operator->() const;
protected: // Properties
CMyObject* m_Object;
t_Bool m_Safe;
}; |
Om de eerste vraag maar vast te stellen: waarom is er hier niets virtual?!? Botweg omdat het niet nodig is cq. niet kan... IsValid en IsInvalid zitten hier in hun echte final implementation, en de andere functies worden allemaal in afgeleide klassen opnieuw gedefinieerd met andere parameterlists (en zijn dus ook final)
Allereerst zien we 3 constructors: eentje die het ding gewoon instantieert, eentje die het resultaat van new CMyObject pikt, en een copy-constructor. Voordat ik op de p_Safe inga moet ik eerst bekennen dat je met Smart Pointers WEL memoryleaks kunt hebben... Stel dat je een class A heeft die een pointer naar class B heeft, en vice versa. Ze hebben allebei dankzij elkaar minimaal een referencecount van 1, en zullen dus nooit als eerste vernietigd worden, en ze blijven dus eeuwig bestaan! Deze circular references komen in een goed opgezet systeem alleen voor in hierarchieen, waarbij een node zowel zn parentnode als zn childnodes moet kennen. In deze situatie moeten de children altijd opgeruimd worden voor de node zelf, en moet de reference naar de parent dus unsafe zijn. Als de flag op false staat doet de node dus niets met de reference count van zn object, maar heb je nog steeds de andere voordelen zoals de toegangsbescherming. De implementaties:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| RMyObject::RMyObject(t_Bool p_Safe)
{
m_Object = NULL;
m_Safe = p_Safe;
}
RMyObject::RMyObject(CMyObject* p_Object)
{
m_Safe = e_True;
m_Object = p_Object;
// Increase reference count
if(m_Object)
m_Object->IncreaseReference();
}
RMyObject::RMyObject(const RMyObject &p_Reference)
{
m_Safe = e_True;
m_Object = p_Reference.m_Object;
// Increase reference count
if(m_Object)
m_Object->IncreaseReference();
} |
De destructor doet simpelweg het omgekeerde: als we naar een object verwijzen, laten we dan object weten dat we hem niet meer nodig hebben:
code:
1
2
3
4
5
6
| RMyObject::~RMyObject()
{
// Decrease reference count on contained object
if(m_Safe && m_Object)
m_Object->DecreaseReference();
} |
Hierna volgen de IsValid en IsInvalid functies, welke simpelweg een Boolean teruggeven of de reference al of niet naar een geldige class wijzen (de implementatie kun je zelf wel bedenken
code:
1
2
3
4
| RMyObject l_AnObject = p_AnObject; if(l_AnObject.IsValid()) l_AnObject->SomeFunctionCall(...); |
De CanConvertFrom en ConvertFrom functies worden pas interessant in afgeleide classes, en er kan hier in de root-class simpelweg volstaan worden met het teruggeven van e_True op de eerste, en een assign uitvoeren op de 2e. In inherited classes gaat dit de automatische dynamic casting verzorgen.
De overloads van de = operator zorgen ervoor dat we andere pointers aan deze kunnen toewijzen. Er is voor zowel voor CMyObject als voor RMyObject een versie omdat we dan zelf de meest geoptimaliseerde versie kunnen geven (deze functie zal miljoenen keren worden aangeroepen dus het is de moeite waard). De volgende implementatie is wederom NIET threadsafe (omdat we botweg de benodigde objecten in ons framework nog niet hebben):
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| void RMyObject::operator = (CMyObject* p_Object)
{
// Decrease reference on previous contained object
if(m_Safe && m_Object)
m_Object->DecreaseReference();
// Assign pointer
m_Object = p_Object;
// Increase reference on contained object
if(m_Safe && m_Object)
m_Object->IncreaseReference();
}
void RMyObject::operator = (const RMyObject &p_Reference)
{
// Decrease reference on previous contained object
if(m_Safe && m_Object)
m_Object->DecreaseReference();
// Assign pointer
m_Object = p_Reference.m_Object;
// Increase reference on contained object
if(m_Safe && m_Object)
m_Object->IncreaseReference();
} |
Je kunt goed zien hier hoe de Safe in het spelletje meedoet: unsafe laten we simpelweg alle referencecounts met rust. De comparison overloads vergelijken enkel het parameter object met het referenced object en retourneren aan de hand hiervan een boolean (vul zelf de implementatie maar in...). Ik heb expres geen overload voor const RMyObject & gedefinieerd, omdat dit met de volgende operator overloads door de compiler impliciet overbodig wordt gemaakt (als je echt wil optimaliseren kun je ze wel toevoegen). Deze overloads emuleren enkele standaardstukjes C++, zoals het dereferencen van een pointer en de address-of operator. Ze zijn als volgt geimplementeerd:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| CMyObject& RMyObject::operator *() const
{
return *m_Object;
}
CMyObject* RMyObject::operator &() const
{
return m_Object;
}
RMyObject::operator CMyObject*() const
{
return m_Object;
} |
Zoals je ziet zijn het knap korte functies, en ze kunnen dus ook makkelijk als inline gedefinieerd worden.
Als laatste komt de ECHT meest gebruikte functie van de reference class: de overload van de -> operator zodat de reference class als een echte pointer functioneert. We checken eerst of er wel een geldig object is, en gooien een exceptie (BMyException noemen we m maar voorlopig, met een string als exception-tekst) indien het niet zo is. Anders geven we de pointer terug:
code:
1
2
3
4
5
6
| CMyObject* RMyObject::operator ->() const
{
if(!m_Object)
throw(BMyException("RMyObject::operator-> did not find a valid reference");
return m_Object;
} |
Simpel toch?
Conclusie
Een programma schrijven op basis van een smart-pointer systeem is vrijwel altijd een goed idee. De voordelen:
-NOOIT meer memoryleaks zolang je op je circular references let.
-NOOIT meer access violations of segmentation faults.
-Cleane makkelijk te lezen code.
-Macht in je destructors.
De nadelen:
-Je programma wordt er een paar procent langzamer door, zon 1-5% afhankelijk van het gebruik en de CPU.
-Let op je circular references
U wilt meer?
Ja, we willen bijvoorbeeld een class afleiden van CMyObject
Voor vragen, gebruik onderstaande ruimte: