Gå til innhold

Anbefalte innlegg

Ordbruk

En klassedeklarasjon forteller kompilatoren at det finnes en klasse med dette navnet, men forteller ikke noe mer. class Person; er et eksempel på en klassedeklarasjon.

 

En klassedefinisjon forteller kompilatoren hvordan en klasse ser ut, og bl.a. hvor stor den er. class Person { /* ... */ }; er en klassedefinisjon.

 

 

Det finnes mange navn på teknikken som blir beskrevet her. Noen av dem er Handle classes og Envelope classes. Klassene som pekes til blir da kalt henholdsvis Body classes og Letter classes. Et annet navn er Cheshire cat classes, som er en allusjon til katten i Alice i eventyrland som kunne etterlate smilet sitt etter at resten av den hadde forsvunnet.

 

 

Forord / historie

Temaet i denne guiden tas opp i

Item 34 ("Minimize compilation dependencies between files") i Effective C++ av Scott Meyers og

§25.7 ("Handle classes") i The C++ Programming Language

 

 

Jeg har hatt lyst til å skrive en bruker-guide her på forumet et stund nå, men jeg har vært litt usikker på hva jeg skal skrive. Jeg har dessuten syntes at det burde være et par guider som tok for seg litt mer avanserte emner enn bare introduksjoner.

 

Så, en kveld for noen uker siden satt jeg og leste noen retningslinjer om Qt/KDE-programmering. Der sto det at klassene skulle allokeres på heap / the free store, altså med operator new. Det syntes jeg at var litt tungvint, og at klassene burde ta seg av minneallokering selv, slik som våre venner i std. Jeg fikk en idé om hvordan jeg kunne implementere dette, og det skal denne guiden ta for seg -- I tillegg til en ganske stor bonus vi får som følge av løsningen.

 

 

Problemet

Ulempen med "vanlige" klasser, sammenlignet med klasser som std::string, er at de inneholder mange mindre variabler, altså enkeltmedlemmer, f.eks. slik:

 

class Person {
   int age;
   std::string name;
   std::string address;
public:
   int get_age() const;
   const std::string& get_name() const;

   // ...
};

 

Dette er dumt hvis vi vil brukere pekere og allokere medlemmene på heap, som vi ønsker i dette tilfellet. Hvorfor? Fordi en int* er like stor som en int (og en char* er større enn en char). Hvis vi i tillegg skal allokere hvert enkelt medlem på heap koster det litt ekstra tid, og det gjør klassen uakseptabelt ineffektiv. Det vi ønsker er å ha én peker som medlem, og dermed bare få ett kall til operator new.

 

 

Løsningen

Måten vi gjør det på er ganske enkel. Vi introduserer en ny klasse som inneholder en peker til klassen vi startet med:

 

class Person_impl {
   int age;
   std::string name;
   std::string address;
public:
   int get_age() const;
   const std::string& get_name() const;

   // ...
};

class Person {
   Person_impl* impl;

   // Kopiering nektes
   Person(const Person&);
   const Person& operator=(const Person&);
public:
   Person();
   ~Person();
   int get_age() const;
   const std::string& get_name() const;
};

 

En ting er viktig å merke seg her. Person inneholder nøyaktig de samme medlemsfunksjonene som Person_impl -- grensesnittene er identiske -- men Person inneholder bare en peker til Person_impl, som tar seg av lagringen av de viktige sakene.

 

Constructoren og Destructoren til Person tar seg selvfølgelig av allokering og deallokering på heap, så brukere av Person-klassen trenger ikke å tenke på det.

 

 

Bonus

Så langt har vi gjort dette for å slippe å allokere Personer for hånd. Men med noen små modifiseringer kan vi få en stor bonus ut av Personen vår. Vi kan nemlig kutte ut en del "kompilasjonsavhengigheter". Hva betyr det? Det betyr at hvis du gjør forandringer i én klasse behøver ikke andre klasser som har noe med den klassen å gjøre nødvendigvis rekompilere. Det betyr ganske enkelt kortere kompilasjonstider. Hemmeligheten er å bytte ut definisjonsavhengigheter med deklarasjonsavhengigheter, bl.a. ved å bruke pekere i steden for "vanlige" klasser. Grunnen til at bruk av pekere gjør om definisjonsavhengigheter til deklarasjonsavhengigheter er ganske enkel. Kompilatoren må vite hvor mye plass den trenger å allokere til et objekt. For å vite det må den se klassedefinisjonen. Men hvis vi bruker en peker vet kompilatoren hvor mye plass den trenger, for en peker er like stor uansett hva den peker til. Constructoren til Person kan ta seg av allokeringen til selve Personen, altså Person_impl, fordi Person har tilgang til klassedefinisjonen til Person_impl.

 

Vi gir Personen vår en fødselsdag av typen Date, for å vise hvordan vi bare skal bruke deklarasjoner, og dermed ser situasjonen slik ut:

 

// person_impl.hpp
#include <string>  // Denne må vi inkludere, det samme
                   // gjelder alt annet i std
class Date;        // Deklarasjon, ikke definisjon

class Person_impl {
   int age;
   Date birthday;
   std::string name;
   std::string address;
public:
   int get_age() const;
   const std::string& get_name() const;

   // ...
};

 

// person.hpp
class Person_impl; // Deklarasjon, ikke definisjon

class Person {
   Person_impl* impl;

   // Kopiering nektes
   Person(const Person&);
   const Person& operator=(const Person&);
public:
   Person();
   ~Person();
   int get_age() const;
   const std::string& get_name() const;
};

 

// person.cpp
#include "person.hpp"
#include "person_impl.hpp" // Her trenger vi å vite detaljene om Person_impl,
                           // fordi vi skal allokere plass til en, vha. new

Person::Person()
{
   impl = new Person_impl;
}

Person::~Person()
{
   delete impl;
}

int Person::get_age() const
{
   return impl->get_age();
}

 

 

 

Det er både fordeler og ulemper ved denne teknikken:

 

Fordeler

- brukere slipper å allokere minne til objektet selv. Det kommer vi tilbake til, ironisk nok, under "ulemper".

- et slikt design minimerer kompilasjonsavhengigheter mellom klasser, som igjen fører til kortere kompilasjonstider

- den eneste implementasjonsdetaljen som evt. er tilgjengelig for brukere er Person_impl* impl;. Det sikrer at brukere ikke tenker på implementasjonen til klassen når de programmerer med den, de tenker bare på grensesnittet. De har ganske enkelt ikke tilgang til Person_impl sin klassedefinisjon.

 

Ulemper

- det koster, både i tid og rom. Klassene kan ikke nyttegjøre seg inline funksjoner, fordi inline funksjoner trenger tilgang til implementasjonsdetaljer til klassen, og en av grunnene til at vi bruker denne teknikken er for å unngå nettopp det. Hvor mye det koster kommer an på, og det er ikke sikkert at det er betydelig. Om det skulle vise seg å være betydelig kan man alltids bytte ut Handle-klassene, altså klassene som inneholder en peker til en _impl-klasse, med klassen de inneholder en peker til. Dette byttet kan skje når programmet er bortimot ferdig -- Man får dermed fordelen av kortere kompilasjonstider og man slipper ekstra utgifter i tid og rom.

- skal man allokere på heap for hånd blir hele teknikken smør på flesk. På den annen side kan brukere få tilgang til en typedef'ed versjon av Person_impl, og dermed allokere på heap uten å gå veien via Person-interface-klassen.

- brukere må selv #include filer de trenger, som f.eks. Date-klassen i eksempelet ovenfor. Sammenlignet med lange kompilasjonstider er det nok verdt det.

Endret av Myubi
Lenke til kommentar
Videoannonse
Annonse

Det er vel kanskje bedre å implementere operator ->() i handle-klassen din. Dette blir vel veldig mye mindre å skrive hvis du har mange funksjoner i Person_impl.

F.eks

class Person_impl {
/* blabla */
public:
   int get_age() const;
   std::string get_name() const;
};

class Person {
   Person_impl* pers;
public:
   int get_age() const;
   std::string get_name() const;
}

int Persong::get_age() const
{
   return pers->get_age();
}

std::string Person::get_name() const
{
   return pers->get-name()
}

Dette er fryktelig slitsomt hvis du skal skrive Person::alle_funksjoner_som_Person_impl_har { return

pers->alle_funksjoner_som_Person_impl_har; }

Da er det lettere å gjøre:

class Person {
Person_impl* pers;
public:
   person_impl* operator->() { return pers; }
};

// dermed kan du bruke den som vanlig i kode
Person ola;
ola->get_age();
ola->get_name();
// osv

 

Du kan jo også legge til ref counting osv

Dett var dett

Lenke til kommentar

Tenkte på å legge til operator->(), men følte at det tok fokus litt vekk fra poenget. Dessuten ville det ha gjort at de 245.945 linjene som er avhengige av Personen min ikke kompilerer ;) (Hvis vi tenker at det var et real-life-scenario).

 

Hva kopiering angår hadde jeg tenkt til å deklarere copy-ctor og operator=() private, men det glemte jeg visst da jeg skreiv guiden (skal fikses). Hadde i bakhodet å evt. skrive en artikkel som implementerte reference-counting senere, hvis guiden fikk god respons.

 

Takk for kritikken :)

Lenke til kommentar
  • 11 måneder senere...

Jeg har også en sen klage å levere, som ikke angår det du prater om.

 

std::string get_name() const;

const std::string& get_name() const;

Ville ha endret til referanse.

 

Ellers vil jeg si at...

// dermed kan du bruke den som vanlig i kode

Person ola;

ola->get_age();

ola->get_name();

..virker rart.

Lenke til kommentar

Opprett en konto eller logg inn for å kommentere

Du må være et medlem for å kunne skrive en kommentar

Opprett konto

Det er enkelt å melde seg inn for å starte en ny konto!

Start en konto

Logg inn

Har du allerede en konto? Logg inn her.

Logg inn nå
  • Hvem er aktive   0 medlemmer

    • Ingen innloggede medlemmer aktive
×
×
  • Opprett ny...