Tutorial Advanced C++: Smart Pointer en frameworks

Pagina: 1
Acties:
  • 236 views sinds 30-01-2008
  • Reageer

  • curry684
  • Registratie: Juni 2000
  • Laatst online: 04-09 14:38

curry684

left part of the evil twins

Topicstarter
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:
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 ;) ). 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:
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 :) 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:

Professionele website nodig?


  • Orphix
  • Registratie: Februari 2000
  • Niet online
Leuk artikel. Ik heb er zelf ook eens over nagedacht, maar nooit toegepast. Ik denk omdat ik mezelf dan teveel toe zou laten op geheugenbeheer gebied ... je gaat te 'slordig' te werk. Voor mezelf stel ik altijd vast wie de object aanmaakt en die is er ook verantwoordelijk voor. Bovendien zijn mijn geheugenleaks meestal zo groot dat ik ze toch wel opmerk :)
Maargoed, ik ben ook gewoon een figuur die zo dicht mogelijk bij de taal blijft (ja, het liefst zou ik geen libraries gebruiken :))
Persoonlijk kick ik wel op dit soort constructies hoor, wat is C++ toch een prachttaal *D

  • stylee
  • Registratie: December 2000
  • Laatst online: 04-09-2021

stylee

blah zeg ik je

ik heb het nog niet gelezen, moet weg.. maar zal dat zeker morgen doen!

bedankt voor het nemen van de moeite om zoiets te schrijven!

  • Nexopheus
  • Registratie: Juni 2001
  • Laatst online: 28-01 13:50
Goed initiatief.
Heb net even copy-pasted gedaan..
Zal het binnen kort gaan lezen en uitproberen.
Ben nml net bezig met Win32 programming en C
(C++).

Bedankt voor de moeite.....

Wat niet kan is nog nooit gebeurd


Verwijderd

Bedankt voor je artikel!

  • Bart Coppens
  • Registratie: April 2000
  • Laatst online: 25-11-2021
Dat kan nog eens goed van pas komen, bedankt!

Copyright Auteur heeft Tweakers.net BV geen exclusieve licentie op bovenstaande post verleend. Voorafgaande en uitdrukkelijke schriftelijke toestemming van Tweakers.net BV is dus niet noodzakelijk voor het vermenigvuldigen van bovenstaande post


  • marcusk
  • Registratie: Februari 2001
  • Laatst online: 26-09-2023
Interessant artikel ! Hoop dat je er nog meer zult plaatsen, zal misschien ook wel bijdragen aan het vergroten van het aantal onderwerpen over C/C++ hier (wat ik zelf ook veel interessanter vind dan al dat php enzo).

Btw, over die naming conventions: de code wordt er idd wel duidelijker op, maar door al die underscores gaat het typen heel wat minder snel :)

  • curry684
  • Registratie: Juni 2000
  • Laatst online: 04-09 14:38

curry684

left part of the evil twins

Topicstarter
Op zondag 08 juli 2001 01:48 schreef marcusk het volgende:
Btw, over die naming conventions: de code wordt er idd wel duidelijker op, maar door al die underscores gaat het typen heel wat minder snel :)
Believe me, je went er verdomd snel aan :Y)

Professionele website nodig?


  • kvdveer
  • Registratie: November 2000
  • Laatst online: 06-11-2025

kvdveer

Z.O.Z.

GREAT!
Erg leerzaam, ik denk dat ik daar eens wat mee ga doen. Ik zit met vreugde te wachten op een vervolg!

Localhost, sweet localhost


  • DeuTeRiuM
  • Registratie: December 1999
  • Laatst online: 13-09 16:19

DeuTeRiuM

http://koelkast.net/

Misschien heb je ook een tip voor boeken/of goede (unix)c++ tutorials. Niet voor de pro, maar gewoon voor iemand die al wat kan maken met perl of php.

If mogelijk nederlands. Anders maar engels.

ik wil nl. zeer graag c++ leren:)

[04:53:17][roel@roel:~]$ sleep
bash: sleep: command not found


  • stylee
  • Registratie: December 2000
  • Laatst online: 04-09-2021

stylee

blah zeg ik je

DeuTeRiuM: Misschien heb je ook een tip voor boeken/of goede (unix)c++ tutorials. Niet voor de pro, maar gewoon voor iemand die al wat kan maken met perl of php.

ik heb ff hier wat dingen voor je online gezet. Hoop dat je er wat aan hebt (andere mensen mogen natuurlijk ook downloaden :) ). Zijn wat tutorials en boeken (zit er ook een voor gevanceerde C++ programmeurs bij).

Op www.bruceeckel.com is ook een hele goede online boek te vinden.

HTH

  • DeuTeRiuM
  • Registratie: December 1999
  • Laatst online: 13-09 16:19

DeuTeRiuM

http://koelkast.net/

DANK!
alleen die link naar die zip doet het.
de ander geeft alleen maar 'connection refused' terug

[04:53:17][roel@roel:~]$ sleep
bash: sleep: command not found


Verwijderd

Op zondag 08 juli 2001 20:03 schreef DeuTeRiuM het volgende:
DANK!
alleen die link naar die zip doet het.
de ander geeft alleen maar 'connection refused' terug
Probeer eens www.bruceeckel.com :)

  • stylee
  • Registratie: December 2000
  • Laatst online: 04-09-2021

stylee

blah zeg ik je

DeuTeRiuM: alleen die link naar die zip doet het.
de ander geeft alleen maar 'connection refused' terug


hmz die link doet het gewoon perfect hoor, een aantal andere mensen hebben hem zelfs gedownload

en die link was idd een spelfoutje.. :) *gefixd*
Pagina: 1