Örvitinn

include skrár í C++

Fjölmargir hafa haft samband við mig undanfarið og grátbeðið um fleiri stefnulausa og illa skrifaða forritunarpistla. Ég get ekki annað en látið undan þessum þrýstingi og bið æsta aðdáendur um að gefa mér nú smá frið :-) Hugsanlega hjálpar að refresha síðuna til að fá örlítið læsilegri C++ kóðadæmi, ég þurfti nefnilega að sóða örlitið í stylesheet skránni.

C++ kóða er skipt upp í tvær tegundir skráa, include skrár sem vanalega enda á .h innihalda skilgreiningar, og svo skrár sem innihalda útfærslu og enda yfirleitt á .cpp.

Mörgum finnst þetta galli á C++ og kjósa frekar að hafa allt í sömu skránni eins og t.d. java og python. Ég sé þó ýmsa kosti við þetta þó þetta krefjist meiri vinnu. Alvöru ritlar auðvelda hana töluvert.

Almenna reglan er sú að í .h skrám eiga bara að vera skilgreiningar og ekkert annað. Stundum setja menn útfærslur á föllum þar inn til að fá þær inline upp á hraða, en ég mæli með því að slíkt sé forðast svo lengi sem hægt er. Betra er að setja inline kóða í sér skrá og include-a hana í .h skrána en ég ætla ekki að fjalla frekar um það hér.

Í h skrám þarf að oftast að inklúda aðrar h skrár til að geta skilgreint klasa eða föll. Almenna reglan varðandi include í h skrám er að maður á að taka inn jafn lítið og maður kemst upp með. Þannig þarf maður að vísa í h skrá fyrir týpu ef maður notar tilvik af týpunni en ekki þegar maður notar bendi eða reference. Ástæðan fyrir því að forðast skal óþarfa vísanir í h skrár er að þær taka mikinn tíma í þýðingu og í stórum forritum getur stór hluti þýðingartímans farið í include mál. Ef maður inklúdar skránni a.h í b.h og breytir svo a.h þarf þýðandinn að endurþýða b.h og allt sem stólar á hana. Ef það include var óþarft er hugsanlega verið að þýða helling af kóða án nokkurrar ástæðu. Einnig er oft verið að gefa óþarfa upplýsingar með því að hafa allt include í h skránni. Notandi einingar á að lesa h skrána til að sjá viðmót hennar, þar eiga alls ekki að koma fram upplýsingar um útfærslu ef hægt er að forðast það. Aftur á móti er nauðsynlegt að vísa í allar h skrár í útfærsluskránni (cpp). Engar aðrar skrár eiga að stóla á útfærlsuskrána, þannig að ekki skiptir jafn miklu máli þó helling sé includeað þar. Vissulega eykst tíminn, en aukningin er línuleg en ekki veldisleg eins og stundum þegar of miklu er includeað í h skrám.

// sull.h
class Sull {
    public:
        Sull();
        ~Sull();
        void foo() const;
    private:
    	int foo;
};

// bull.h
// nauðsynlegt að inklúda þar sem tilvik af Sull er notað
#include "sull.h"  class Bull {
    public:
        Bull();
        ~Bull();
        void foo() const;
    private:
    	Sull s;
};

// drull.h
// nauðsynlegt að inklúda þar sem klasinn erfir frá Sull
#include "sull.h"  
class Drull : public Sull {
    public:
        Bull();
        ~Bull();
};

// sullumbull.h
class Sull;  
// Hér þarf ekki að inklúda þar sem bendir á týpuna 
// er notaður, því er nóg að fyrirfram skilgreina klasann
class SullumBull {
    public:
        SullumBull();
        ~SullumBull();
        void foo() const;
    private:
    	Sull* s;
};

// sullumbull.cpp
#include "sullumbull.h"
// þurfum að inklúda henni í útfærsluskránni.
#include "sull.h"		
SullumBull::SullumBull()
{
    s = new Sull;
}


Ástæðan fyrir því það er nauðsynlegt að inklúda skrám þegar verið er að nota tilvik eða erfa frá týpu er að þýðandinn þarf þá að vita hvernig týpan er útfærð í minni. Ef það er aftur á móti verið að nota bendi eða reference veit þýðandinn hversu mikið pláss þarf þar sem bendar eru alltaf jafn stórir, sama hvað þeir benda á. Það er ekki fyrr en kemur að útfærslunni sem meiri upplýsinga er þörf fyrir þýðandann í þeim tilvikum.

Skoðum dæmi um hvað illa ígrunduð notkun á include getur haft í för með sér.

// a.h
// vector klasinn er einungis notaður við útfærslu þessa 
//klasa en ekki í viðmóti, samt inklúdað í h skrá
#include <vector> 
class A
{
    public:
        void doSomeStuff(int) const
};

// b.cpp
std::vector<int> intVector;
A::doSomeStuff(int stuff) {
    intVector.push_back(stuff)

// b.h
#include "a.h" //notum A reference en inklúdum samt
class B
{
    public:
        int stuffCounter(const A&);
};

// c.h
#include "a.h" //notum B bendi en inklúdum samt
class C
{
    public:
        void foo();
    private:
        B* b;
};

Ef við á einhverju stigi ákveðum að breyta úfærlsunni á A þannig að í stað þess að nota vector ætlum við að nota map þurfum við að endurþýða allt draslið aftur þrátt fyrir að ekkert hafi breyst í viðmóti klasanna sem við erum að nota.

Það er oft hentugt að nota fyrirfram skilgreiningar á klösum í stað þess að inklúda skrána. Fyrirfram skilgreining er gerð með því að skrifa einfaldlega class Nafn; áður en vísað er á týpuna. Í útfærsluskránni þarf svo að nota include.

Þeir sem hafa lesið til enda eiga hrós skilið. Ef þetta er óskiljanlegt eða algjört bull, sem mig grunar, setjið þá endilega inn athugasemd. Ef þið eruð ósammála því sem hér er sagt, setjið þá inn athugasemd.

17.12.03 13:30
Jón Arnar bendir réttilega á að það þarf að gera undantekningar þegar kemur að templates. Ég get ekki sett kóðadæmi í athugasemdir og nenni ekki að stilla comment filterinn, þannig að þetta dæmi tengist athugasemdinni minni hér fyrir neðan. Sama gildir um inline föll sem ég minnist á í upphafi.


// útfærsla inline
template<class T> class Plus
{
class="k">public:
    T reikna(const T& a, const T& b) const
    {
        return a+b;
    }
};

// útfærsla sér
template<class T> class Minus
{
public:
    T reikna(const T& a, const T& b) const;
};

// útfærsla, gæti verið í sér skrá og include-að í h skrá
// eða einfaldlega neðar í sömu h skránni
template <class T> 
T Minus<T>::reikna(const T& a, const T& b) const
{
    return a-b;
}

c++
Athugasemdir

jonarnar - 17/12/03 11:14 #

Fyrirtaks færsla ! Mig langar samt að benda á einn agnúa sem brýtur þessi skil milli .h og .cpp skráa (allaveganna með g++). Þegar templete clasar eru forritaðir þarf að útfæra föllin í .h skránni. Ég hef allaveganna ekki náð að skúbba því yfir í sér skrá ...

Matti Á. - 17/12/03 11:27 #

Góður punktur. Það er rétt að template kóði þarf allur að vera inline, a.m.k. veit ég ekki um neinn þýðanda sem gefur manni kost á að splitta því upp.

Það er ágæt regla að aðskilja viðmót og útfærslu template kóða með því að hafa skilgreininguna sér og útfærsluna neðar í skránni, þ.e.a.s. ekki hafa útfærsluna í klasaskilgreiningunni. Þá er hægt að færa útfærsluna í sér skrá og vísa svo í hana í h skránni. Þá fær maður að minnsta kosti aðskilnað á skilgreiningu og útfærslu fyrir notendur einingar, þó þýðandinn komist ekki hjá því að lesa alla súpuna inn í hvert skipti sem nýtt template tag er notað.

Matti Á. - 17/12/03 13:30 #

Ég bætti inn smá kóðadæmi fyrir template.

Í seinna dæminu er vissulega mun meiri kóði en overheadið er föst stærð, þannig að um leið og útfærslan er flóknari, meira en 1-2 línur, skiptir þetta overhead minna máli. Sérstaklega ef þessi kóði er færður í sér skrá, þar sem þá minnkar kóðinn í h skránni verulega, sem er gott.