136 36 4MB
Hungarian Pages 1291 Year 2001
Bevezetés
Ez a bevezetés áttekintést ad a C++ programozási nyelv fõ fogalmairól, tulajdonságairól és standard (szabvány) könyvtáráról, valamint bemutatja a könyv szerkezetét és elmagyarázza azt a megközelítést, amelyet a nyelv lehetõségeinek és azok használatának leírásánál alkalmaztunk. Ezenkívül a bevezetõ fejezetek némi háttérinformációt is adnak a C++-ról, annak felépítésérõl és felhasználásáról.
Fejezetek 1. Megjegyzések az olvasóhoz 2. Kirándulás a C++-ban 3. Kirándulás a standard könyvtárban
1 Megjegyzések az olvasóhoz
Szólt a Rozmár: Van ám elég, mirõl mesélni jó: ... (L. Carroll ford. Tótfalusi István)
A könyv szerkezete Hogyan tanuljuk a C++-t? A C++ jellemzõi Hatékonyság és szerkezet Filozófiai megjegyzés Történeti megjegyzés Mire használjuk a C++-t? C és C++ Javaslatok C programozóknak Gondolatok a C++ programozásról Tanácsok Hivatkozások
1.1. A könyv szerkezete A könyv hat részbõl áll: Bevezetés: Elsõ rész: Második rész:
Az 13. fejezetek áttekintik a C++ nyelvet, az általa támogatott fõ programozási stílusokat, és a C++ standard könyvtárát. A 49. fejezetek oktató jellegû bevezetést adnak a C++ beépített típusairól és az alapszolgáltatásokról, melyekkel ezekbõl programot építhetünk. A 1015. fejezetek bevezetést adnak az objektumorientált és az általánosított programozásba a C++ használatával.
4
Bevezetés
Harmadik rész: A 1622. fejezetek bemutatják a C++ standard könyvtárát. Negyedik rész: A 2325. fejezetek tervezési és szoftverfejlesztési kérdéseket tárgyalnak. Függelékek: Az AE függelékek a nyelv technikai részleteit tartalmazzák. Az 1. fejezet áttekintést ad a könyvrõl, néhány ötletet ad, hogyan használjuk, valamint háttérinformációkat szolgáltat a C++-ról és annak használatáról. Az olvasó bátran átfuthat rajta, elolvashatja, ami érdekesnek látszik, és visszatérhet ide, miután a könyv más részeit elolvasta. A 2. és 3. fejezet áttekinti a C++ programozási nyelv és a standard könyvtár fõ fogalmait és nyelvi alaptulajdonságait, megmutatva, mit lehet kifejezni a teljes C++ nyelvvel. Ha semmi mást nem tesznek, e fejezetek meg kell gyõzzék az olvasót, hogy a C++ nem (csupán) C, és hogy a C++ hosszú utat tett meg e könyv elsõ és második kiadása óta. A 2. fejezet magas szinten ismertet meg a C++-szal. A figyelmet azokra a nyelvi tulajdonságokra irányítja, melyek támogatják az elvont adatábrázolást, illetve az objektumorientált és az általánosított programozást. A 3. fejezet a standard könyvtár alapelveibe és fõ szolgáltatásaiba vezet be, ami lehetõvé teszi, hogy a szerzõ a standard könyvtár szolgáltatásait használhassa a következõ fejezetekben, valamint az olvasónak is lehetõséget ad, hogy könyvtári szolgáltatásokat használjon a gyakorlatokhoz és ne kelljen közvetlenül a beépített, alacsony szintû tulajdonságokra hagyatkoznia. A bevezetõ fejezetek egy, a könyv folyamán általánosan használt eljárás példáját adják: ahhoz, hogy egy módszert vagy tulajdonságot még közvetlenebb és valószerûbb módon vizsgálhassunk, alkalmanként elõször röviden bemutatunk egy fogalmat, majd késõbb behatóbban tárgyaljuk azt. Ez a megközelítés lehetõvé teszi, hogy konkrét példákat mutassunk be, mielõtt egy témát általánosabban tárgyalnánk. A könyv felépítése így tükrözi azt a megfigyelést, hogy rendszerint úgy tanulunk a legjobban, ha a konkréttól haladunk az elvont felé még ott is, ahol visszatekintve az elvont egyszerûnek és magától értetõdõnek látszik. Az I. rész a C++-nak azt a részhalmazát írja le, mely a C-ben vagy a Pascalban követett hagyományos programozási stílusokat támogatja. Tárgyalja a C++ programokban szereplõ alapvetõ típusokat, kifejezéseket, vezérlési szerkezeteket. A modularitást, mint a névterek, forrásfájlok és a kivételkezelés által támogatott tulajdonságot, szintén tárgyalja. Feltételezzük, hogy az olvasónak már ismerõsek az I. fejezetben használt alapvetõ programozási fogalmak, így például bemutatjuk a C++ lehetõségeit a rekurzió és iteráció kifejezésére, de nem sokáig magyarázzuk, milyen hasznosak ezek. A II. rész a C++ új típusok létrehozását és használatát segítõ szolgáltatásait írja le. Itt (10. és 12. fejezet) mutatjuk be a konkrét és absztrakt osztályokat (felületeket), az operátor-túlterheléssel (11. fejezet), a többalakúsággal (polimorfizmussal) és az osztályhierarchiák hasz-
1. Megjegyzések az olvasóhoz
5
nálatával (12. és 15. fejezet) együtt. A 13. fejezet a sablonokat (template) mutatja be, vagyis a C++ lehetõségeit a típus- és függvénycsaládok létrehozására, valamint szemlélteti a tárolók elõállítására (pl. listák), valamint az általánosított (generikus) programozás támogatására használt alapvetõ eljárásokat. A 14. fejezet a kivételkezelést, a hibakezelési módszereket tárgyalja és a hibatûrés biztosításához ad irányelveket. Feltételezzük, hogy az olvasó az objektumorientált és az általánosított programozást nem ismeri jól, illetve hasznát látná egy magyarázatnak, hogyan támogatja a C++ a fõ elvonatkoztatási (absztrakciós) eljárásokat. Így tehát nemcsak bemutatjuk az elvonatkoztatási módszereket támogató nyelvi tulajdonságokat, hanem magukat az eljárásokat is elmagyarázzuk. A IV. rész ebben az irányban halad tovább. A III. rész a C++ standard könyvtárát mutatja be. Célja: megértetni, hogyan használjuk a könyvtárat; általános tervezési és programozási módszereket szemléltetni és megmutatni, hogyan bõvítsük a könyvtárat. A könyvtár gondoskodik tárolókról (konténerek list, vector, map, 18. és 19. fejezet), szabványos algoritmusokról (sort, find, merge, 18. és 19. fejezet), karakterlánc-típusokról és -mûveletekrõl (20. fejezet), a bemenet és kimenet kezelésérõl (input/output, 21. fejezet), valamint a számokkal végzett mûveletek (numerikus számítás) támogatásáról (22. fejezet). A IV. rész olyan kérdéseket vizsgál, melyek akkor merülnek fel, amikor nagy szoftverrendszerek tervezésénél és kivitelezésénél a C++-t használjuk. A 23. fejezet tervezési és vezetési kérdésekkel foglalkozik. A 24. fejezet a C++ programozási nyelv és a tervezési kérdések kapcsolatát vizsgálja, míg a 25. fejezet az osztályok használatát mutatja be a tervezésben. Az A függelék a C++ nyelvtana, néhány jegyzettel. A B függelék a C és a C++ közti és a szabványos C++ (más néven ISO C++, ANSI C++) illetve az azt megelõzõ C++-változatok közti rokonságot vizsgálja. A C függelék néhány nyelvtechnikai példát mutat be, A D függelék pedig a kulturális eltérések kezelését támogató standard könyvtárbeli elemeket mutatja be. Az E függelék a standard könyvtár kivételkezelésel kapcsolatos garanciáit és követelményeit tárgyalja.
1.1.1. Példák és hivatkozások Könyvünk az algoritmusok írása helyett a program felépítésére fekteti a hangsúlyt. Következésképpen elkerüli a ravasz vagy nehezebben érthetõ algoritmusokat. Egy egyszerû eljárás alkalmasabb az egyes fogalmak vagy a programszerkezet egy szempontjának szemléltetésére. Például Shell rendezést használ, ahol a valódi kódban jobb lenne gyorsrendezést (quicksort) használni. Gyakran jó gyakorlat lehet a kód újraírása egy alkalmasabb algoritmussal. A valódi kódban általában jobb egy könyvtári függvény hívása, mint a könyvben használt, a nyelvi tulajdonságok szemléltetésére használt kód.
6
Bevezetés
A tankönyvi példák szükségszerûen egyoldalú képet adnak a programfejlesztésrõl. Tisztázva és egyszerûsítve a példákat a felmerült bonyolultságok eltûnnek. Nincs, ami helyettesítené a valódi programok írását, ha benyomást akarunk kapni, igazából milyen is a programozás és egy programozási nyelv. Ez a könyv a nyelvi tulajdonságokra és az alapvetõ eljárásokra összpontosít, amelyekbõl minden program összetevõdik, valamint az összeépítés szabályaira. A példák megválasztása tükrözi fordítóprogramokkal, alapkönyvtárakkal, szimulációkkal jellemezhetõ hátteremet. A példák egyszerûsített változatai a valódi kódban találhatóknak. Egyszerûsítésre van szükség, hogy a programozási nyelv és a tervezés lényeges szempontjai el ne vesszenek a részletekben. Nincs ügyes példa, amelynek nincs megfelelõje a valódi kódban. Ahol csak lehetséges, a C függelékben lévõ nyelvtechnikai példákat olyan alakra hoztam, ahol a változók x és y, a típusok A és B, a függvények f() és g() nevûek. A kódpéldákban az azonosítókhoz változó szélességû betûket használunk. Például: #include int main() { std::cout =
// egyenlõ // nem egyenlõ // kisebb // nagyobb // kisebb vagy egyenlõ // nagyobb vagy egyenlõ
Értékadásokban és aritmetikai mûveletekben a C++ az alaptípusok között elvégez minden értelmes átalakítást, így azokat egymással tetszés szerint keverhetjük:
32
Bevezetés
void some_function() { double d = 2.2; int i = 7; d = d+i; i = d*i; }
// értéket vissza nem adó függvény // lebegõpontos szám kezdeti értékadása // egész kezdeti értékadása // összeg értékadása // szorzat értékadása
Itt = az értékadó mûvelet jele és == az egyenlõséget teszteli, mint a C-ben.
2.3.2. Elágazások és ciklusok A C++ az elágazások és ciklusok kifejezésére rendelkezik a hagyományos utasításkészlettel. Íme egy egyszerû függvény, mely a felhasználótól választ kér és a választól függõ logikai értéket ad vissza: bool accept() { cout > answer;
}
// kérdés kiírása // válasz beolvasása
if (answer == 'y') return true; return false;
A > (olvasd be) a bemenet mûveleti jele, a cin a szabványos bemenõ adatfolyam. A >> jobb oldalán álló kifejezés határozza meg, milyen bemenet fogadható el és ez a beolvasás célpontja. A \n karakter a kiírt karakterlánc végén új sort jelent. A példa kissé javítható, ha egy 'n' választ is számításba veszünk: bool accept2() { cout > answer;
// kérdés kiírása // válasz beolvasása
2. Kirándulás a C++-ban
}
33
switch (answer) { case 'y': return true; case 'n': return false; default: cout answer;
// kérdés kiírása // válasz beolvasása
switch (answer) { case 'y': return true; case 'n': return false; default: cout > átugorja az üreshely (whitespace) karaktereket, az ilyenekkel elválasztott egészeket az alábbi egyszerû ciklussal olvashatjuk be: int read_ints(vector& v) // feltölti v-t , visszatér a beolvasott egészek számával { int i = 0; while (i>v[i]) i++; return i; }
Ha a bemeneten egy nem egész érték jelenik meg, a bemeneti mûvelet hibába ütközik, így a ciklus is megszakad. Például ezt a bemenetet feltételezve: 1 2 3 4 5.6 7 8.
a read_ints() függvény öt egész számot fog beolvasni: 12345
A mûvelet után a bemeneten a következõ beolvasható karakter a pont lesz. A nem látható üreshely (whitespace) karakterek meghatározása itt is ugyanaz, mint a szabványos C-ben (szóköz, tabulátor, újsor, függõleges tabulátorra illesztés, kocsivissza), és a fejállományban levõ isspace() függvénnyel ellenõrizhetõk valamely karakterre (§20.4.2). Az istream objektumok használatakor a leggyakoribb hiba, hogy a bemenet nem egészen abban a formátumban érkezik, mint amire felkészültünk, ezért a bemenet nem történik meg. Ezért mielõtt használni kezdenénk azokat az értékeket, melyeket reményeink szerint beolvastunk, ellenõriznünk kell a bemeneti adatfolyam állapotát (§21.3.3), vagy kivételeket kell használnunk (§21.3.6). A bemenethez használt formátumot a helyi sajátosságok (locale) határozzák meg (§21.7). Alapértelmezés szerint a logikai értékeket a 0 (hamis) és az 1 (igaz) érték jelzi, az egészeket tízes számrendszerben kell megadnunk, a lebegõpontos számok formája pedig olyan, ahogy a C++ programokban írhatjuk azokat. A basefield (§21.4.2) tulajdonság beállításával lehetõség van arra is, hogy a 0123 számot a 83 tízes számrendszerbeli szám oktális alakjaként, a 0xff bemenetet pedig a 255 hexadecimális alakjaként értelmezzük. A mutatók beolvasásához használt formátum teljesen az adott nyelvi változattól függ (nézzünk utána, saját fejlesztõrendszerünk hogyan mûködik).
826
A standard könyvtár
Meglepõ módon nincs olyan >> tagfüggvény, mellyel egy karaktert olvashatnánk be. Ennek oka az, hogy a >> operátor a karakterekhez a get() karakter-beolvasó függvény (§21.3.4) segítségével könnyen megvalósítható, így nem kell tagfüggvényként szerepelnie. Az adatfolyamokról saját karaktertípusaiknak megfelelõ karaktereket olvashatunk be. Ha ez a karaktertípus a char, akkor beolvashatunk signed char és unsigned char típusú adatot is: template basic_istream& operator>>(basic_istream&, Ch&); template basic_istream& operator>>(basic_istream&, unsigned char&); template basic_istream& operator>>(basic_istream&, signed char&);
A felhasználó szempontjából teljesen mindegy, hogy a >> tagfüggvény vagy önálló eljárás-e. A többi >> operátorhoz hasonlóan ezek a függvények is elõször átugorják a bevezetõ üreshely karaktereket: void f() { char c; cin >> c; // ... }
Ez a kódrészlet az elsõ nem üreshely karaktert a cin adatfolyamból a c változóba helyezi. A beolvasás célja lehet karaktertömb is: template basic_istream& operator>>(basic_istream&, Ch*); template basic_istream& operator>>(basic_istream&, unsigned char*); template basic_istream& operator>>(basic_istream&, signed char*);
Ezek a mûveletek is eldobják a bevezetõ üreshely karaktereket, majd addig olvasnak, amíg egy üreshely karakter vagy fájlvége jel nem következik, végül a karaktertömb végére egy 0 karaktert írnak. Természetesen ez a megoldás alkalmat ad a túlcsordulásra, így érdemesebb inkább egy string objektumba (§20.3.15) helyezni a beolvasott adatokat. Ha mégis az elõbbi
21. Adatfolyamok
827
megoldást választjuk, rögzítsük, hogy legfeljebb hány karaktert akarunk beolvasni a >> operátor segítségével. Az is.width(n) függvényhívással azt határozzuk meg, hogy a következõ, is bemeneti adatfolyamon végrehajtott >> mûvelet legfeljebb n-1 karaktert olvashat be: void g() { char v[4]; cin.width(4); cin >> v; cout >buf) os buf mûvelet egy referenciát ad vissza, amely az is adatfolyamra hivatkozik, a vizsgálatot pedig az is::operator void*() mûvelet végzi: void f(istream& i1, istream& { iocopy(i1,cout); iocopy(i2,cout); iocopy(i3,cout); iocopy(i4,cout); }
i2, istream& i3, istream& i4) // komplex számok másolása // kétszeres pontosságú lebegõpontos számok másolása // karakterek másolása // üreshelyekkel elválasztott szavak másolása
21.3.4. Karakterek beolvasása A >> operátor formázott bemenetre szolgál, tehát adott típusú és adott formátumú objektumok beolvasására. Ha erre nincs szükségünk és inkább valóban karakterekként akarjuk beolvasni a karaktereket, hogy késõbb mi dolgozzuk fel azokat, használjuk a get() függvényeket: template class basic_istream : virtual public basic_ios { public: // ... // formázatlan bemenet streamsize gcount() const;
// a legutóbbi get() által beolvasott karakterek száma
int_type get();
// egy Ch (vagy Tr::eof()) beolvasása
basic_istream& get(Ch& c);
// egy Ch olvasása c-be
basic_istream& get(Ch* p, streamsize n); basic_istream& get(Ch* p, streamsize n, Ch term);
// újsor a lezárójel
830
A standard könyvtár
basic_istream& getline(Ch* p, streamsize n); // újsor a lezárójel basic_istream& getline(Ch* p, streamsize n, Ch term);
};
basic_istream& ignore(streamsize n = 1, int_type t = Tr::eof()); basic_istream& read(Ch* p, streamsize n); // legfeljebb n karakter beolvasása // ...
Ezek mellett a fejállomány biztosítja a getline() függvényt is, amely a szabványos string-ek (§20.3.15) beolvasására használható. A get() és a getline() függvények ugyanúgy kezelik az üreshely karaktereket, mint bármely más karaktert. Kifejezetten olyan adatok beolvasására szolgálnak, ahol nem számít a beolvasott karakterek jelentése. Az istream::get(char&) függvény egyetlen karaktert olvas be paraméterébe. Egy karakterenként másoló programot például a következõképpen írhatunk meg. int main() { char c; while(cin.get(c)) cout.put(c); }
A háromparaméterû s.get(p,n,term) legfeljebb n-1 karaktert olvas be a p[0],
, p[n-2] tömbbe. A get() az átmeneti tárban mindenképpen elhelyez egy 0 karaktert a beolvasott karakterek után, így a p mutatónak egy legalább n karakter méretû tömbre kell mutatnia. A harmadik paraméter (term) az olvasást lezáró karaktert határozza meg. A háromparaméterû get() leggyakoribb felhasználási módja az, hogy beolvasunk egy sort egy rögzített méretû tárba és késõbb innen használjuk fel karaktereit: void f() { char buf[100]; cin >> buf; cin.get(buf,100,'\n'); // ... }
// gyanús: túlcsordulhat // biztonságos
Ha a get() megtalálja a lezáró karaktert, az adatfolyamban hagyja, tehát a következõ bemeneti mûvelet ezt a karaktert kapja meg elsõként. Soha ne hívjuk meg újra a get() függvényt a lezáró karakter eltávolítása nélkül. Ha egy get() vagy getline() függvény egyetlen karaktert sem olvas és távolít el az adatfolyamról meghívódik setstate(failbit), így a következõ beolvasások sikertelenek lesznek (vagy kivétel váltódik ki, 21.3.6).
21. Adatfolyamok
831
void subtle_error { char buf[256];
}
while (cin) { cin.get(buf,256); // a sor beolvasása cout (istream& s, complex& a) /* a complex típus lehetséges bemeneti formátumai ("f" lebegõpontos szám) f (f) (f,f) */ {
834
A standard könyvtár
double re = 0, im = 0; char c = 0; s >> c; if (c == '(') { s >> re >> c; if (c == ',') s >> im >> c; if (c != ')') s.clear(ios_base::failbit); } else { s.putback(c); s >> re; }
}
// állapot beállítása
if (s) a = complex(re,im); return s;
A nagyon kevés hibakezelõ utasítás ellenére ez a programrészlet szinte minden hibatípust képes kezelni. A lokális c változónak azért adunk kezdõértéket, hogy ha az elsõ >> mûvelet sikertelen, nehogy véletlenül pont a ( karakter legyen benne. Az adatfolyam állapotának ellenõrzésére a függvény végén azért van szükség, mert az a paraméter értékét csak akkor változtathatjuk meg, ha a korábbi mûveleteket sikeresen végrehajtottuk. Ha formázási hiba történik az adatfolyam állapota failbit lesz. Azért nem badbit, mert maga az adatfolyam nem sérült meg. A felhasználó a clear() függvénnyel alapállapotba helyezheti az adatfolyamot és továbblépve értelmes adatokat nyerhet onnan. Az adatfolyam állapotának beállítására szolgáló függvény neve clear(), mert általában arra használjuk, hogy az adatfolyam állapotát good() értékre állítsuk. Az ios_base::clear() (§21.3.3) paraméterének alapértelmezett értéke ios_base::goodbit.
21.3.6. Kivételek Nagyon kényelmetlen minden egyes I/O mûvelet után külön ellenõrizni, hogy sikeres volte, ezért nagyon gyakori hiba, hogy elfelejtünk egy ellenõrzést ott, ahol feltétlenül szükség van rá. A kimeneti mûveleteket általában nem ellenõrzik, annak ellenére, hogy néha ott is elõfordulhat hiba. Egy adatfolyam állapotának közvetlen megváltoztatására csak a clear() függvényt használhatjuk. Ezért ha értesülni akarunk az adatfolyam állapotának megváltozásáról, elég nyilvánvaló módszer, hogy a clear() függvényt kivételek kiváltására kérjük. Az ios_base osztály exceptions() tagfüggvénye pontosan ezt teszi:
21. Adatfolyamok
835
template class basic_ios : public ios_base { public: // ...
};
class failure;
// kivételosztály (lásd §14.10)
iostate exceptions() const; void exceptions(iostate except);
// kivétel-állapot kiolvasása // kivétel-állapot beállítása
// ...
A következõ utasítással például elérhetjük, hogy a clear() egy ios_base::failure kivételt váltson ki, ha a cout adatfolyam bad, fail vagy eof állapotba kerül, vagyis ha valamelyik mûvelet nem hibátlanul fut le: cout.exceptions(ios_base::badbit|ios_base::failbit|ios_base::eofbit);
Ha szükség van rá, a cout vizsgálatával pontosan megállapíthatjuk, milyen probléma történt. Ehhez hasonlóan a következõ utasítással azokat a ritkának egyáltalán nem nevezhetõ eseteket dolgozhatjuk fel, amikor a beolvasni kívánt adatok formátuma nem megfelelõ, és ennek következtében a bemeneti mûvelet nem ad vissza értéket: cin.exceptions(ios_base::badbit|ios_base::failbit);
Ha az exceptions() függvényt paraméterek nélkül hívjuk meg, akkor azokat az I/O állapotjelzõket adja meg, amelyek kivételt váltanak ki: void print_exceptions(ios_base& ios) { ios_base::iostate s = ios.exceptions(); if (s&ios_base::badbit) cout s;
utasítássorozat egyenértékû az alábbival: cout > s;
Adott idõben minden adatfolyamhoz legfeljebb egy ostream objektum köthetõ. Az s.tie(0) utasítással leválaszthatjuk az s objektumhoz kötött adatfolyamot (ha volt ilyen). A többi olyan adatfolyam-függvényhez hasonlóan, melyek értéket állítanak be, a tie(s) is a korábbi értéket adja vissza, tehát a legutóbb ide kötött adatfolyamot, vagy ha ilyen nincs, akkor a 0 értéket. Ha a tie() függvényt paraméterek nélkül hívjuk meg, akkor egyszerûen visszakapjuk az aktuális értéket, annak megváltoztatása nélkül. A szabványos adatfolyamok esetében a cout hozzá van kötve a cin bemenethez, a wcout pedig a wcin adatfolyamhoz. A cerr adatfolyamot felesleges lenne bármihez is hozzákötni, mivel ehhez nincs átmeneti tár, a clog pedig nem vár felhasználói közremûködést.
838
A standard könyvtár
21.3.8. Õrszemek Amikor a > operátort a complex típusra használtuk, egyáltalán nem foglalkoztunk az összekötött adatfolyamok (§21.3.7) kérdésével, vagy azzal, hogy az adatfolyam állapotának megváltozása kivételeket okoz-e (§21.3.6). Egyszerûen azt feltételeztük (és nem ok nélkül), hogy a könyvtár által kínált függvények figyelnek helyettünk ezekre a problémákra. De hogyan képesek erre? Néhány tucat ilyen függvénnyel kell megbirkóznunk, így ha olyan bonyolult eljárásokat kellene készítenünk, amely az összekötött adatfolyamokkal, a helyi sajátosságokkal (locale, §21.7, §D), a kivételekkel, és egyebekkel is foglalkoznak, akkor igen kusza kódot kapnánk. A megoldást a sentry (õrszem) osztály bevezetése jelenti, amely a közös kódrészleteket tartalmazza. Azok a részek, melyeknek elsõként kell lefutniuk (a prefix kód, például egy lekötött adatfolyam kiürítése), a sentry konstruktorában kaptak helyet. Az utolsóként futó sorokat (a suffix kódokat, például az állapotváltozások miatt elvárt kivételek kiváltását) a sentry destruktora határozza meg: template class basic_ostream : virtual public basic_ios { // ... class sentry; // ... }; template class basic_ostream::sentry { public: explicit sentry(basic_ostream& s); ~sentry(); operator bool(); };
// ...
Tehát egy általános kódot írtunk, melynek segítségével az egyes függvények a következõ formába írhatók: template basic_ostream& basic_ostream::operators>>x) try { current = m.insert(current,make_pair(s,x)); } catch(...) { // itt a "current" még mindig az aktuális elemet jelöli } }
1290
Függelékek és tárgymutató
E.4.2. Garanciák és kompromisszumok Az alapbiztosításon túli szolgáltatások összevisszaságai a megvalósítási lehetõségekkel magyarázhatók. A programozók azt szeretnék leginkább, hogy mindenhol erõs biztosítás álljon rendelkezésükre a lehetõ legkevesebb korlátozás mellett, de ugyanakkor azt is elvárják, hogy a standard könyvtár minden mûvelete optimálisan hatékony legyen. Mindkét elvárás jogos, de sok mûvelet esetében lehetetlen egymással párhuzamosan megvalósítani. Ahhoz, hogy jobban megvilágítsuk az elkerülhetetlen kompromisszumokat, megvizsgáljuk, milyen módokon lehet egy vagy több elemet felvenni egy listába, vektorba vagy map-be. Nézzük elõször, hogy egy elemet hogyan vihetünk be egy listába vagy egy vektorba. Szokás szerint, a push_back() nyújtja a legegyszerûbb lehetõséget: void f(list& lst, vector& vec, const X& x) { try { lst.push_back(x); // hozzáadás a listához } catch (...) { // lst változatlan return; } try { vec.push_back(x); // hozzáadás a vektorhoz } catch (...) { // vec változatlan return; } // lst és vec egy-egy x értékû új elemmel rendelkezik }
Az erõs biztosítás megvalósítása ez esetben egyszerû és olcsó. Az eljárás azért is hasznos, mert teljesen kivételbiztos megoldást ad az elemek felvételére. A push_back() azonban asszociatív tárolókra nem meghatározott: a map osztályban nincs back(). Egy asszociatív tároló esetében az utolsó elemet a rendezés határozza meg, nem a pozíció. Az insert() függvény garanciái már kicsit bonyolultabbak. A gondot az jelenti, hogy az insert() mûveletnek gyakran kell egy elemet a tároló közepén elhelyeznie. Láncolt adatszerkezeteknél ez nem jelent problémát, tehát a list és a map egyszerûen megvalósítható, a vector esetében azonban elõre lefoglalt terület áll rendelkezésünkre, a vector::insert() függvény egy átlagos megvalósítása pedig a beszúrási pont utáni elemeket áthelyezi, hogy helyet csináljon az új elem számára. Ez az optimális megoldás, de arra nincs egyszerû mód-
E. Kivételbiztosság a standard könyvtárban
1291
szer, hogy a vektort visszaállítsuk eredeti állapotába, ha valamelyik elem másoló értékadása vagy másoló konstruktora kivételt vált ki (lásd §E.8[10-11]), ezért a vector azzal a feltétellel ad biztosításokat, hogy az elemek másoló konstruktora nem vált ki kivételt. A list és a map osztálynak nincs szüksége ilyen korlátozásra, ezek könnyedén be tudják illeszteni az új elemet a szükséges másolások elvégzése után. Példaképpen tételezzük fel, hogy az X másoló konstruktora és másoló értékadása egy X::cannot_copy kivételt vált ki, ha valamilyen okból nem sikerül létrehoznia a másolatot: void f(list& lst, vector& vec, map& m, const X& x, const string& s) { try { lst.insert(lst.begin(),x); // hozzáadás a listához } catch (...) { // lst változatlan return; } try { vec.insert(vec.begin(),x); // hozzáadás a vektorhoz } catch (X::cannot_copy) { // hoppá: vec vagy rendelkezik, vagy nem rendelkezik új elemmel return; } catch (...) { // vec változatlan return; } try { m.insert(make_pair(s,x)); // hozzáadás az asszociatív tömbhöz } catch (...) { // m változatlan return; } // lst és vec egy-egy x értékû új elemmel rendelkezik // m egy új (s,x) értékû elemmel rendelkezik }
Ha X::cannot_copy kivételt kapunk, nem tudhatjuk, hogy az új elem bekerült-e a vec tárolóba. Ha sikerült beilleszteni az elemet, az érvényes állapotban lesz, de pontos értékét nem ismerjük. Az is elképzelhetõ, hogy egy X::cannot_copy kivétel után néhány elem titokza-
1292
Függelékek és tárgymutató
tosan megkettõzõdik (lásd §E.8[11]), másik megvalósítást alkalmazva pedig a vektor végén lévõ elemek tûnhetnek el, mert csak így lehet biztosítani, hogy a tároló érvényes állapotban maradjon és ne szerepeljenek benne érvénytelen elemek. Sajnos az erõs biztosítás megvalósítása a vector osztály insert() függvénye esetében lehetetlen, ha megengedjük, hogy az elemek másoló konstruktora kivételt váltson ki. Ha egy vektorban teljesen meg akarnánk védeni magunkat az elemek áthelyezése közben keletkezõ kivételektõl, a költségek elviselhetetlenül megnõnének az egyszerû, alapbiztosítást nyújtó megoldáshoz képest. Sajnos nem ritkák az olyan elemtípusok, melyek másoló konstruktora kivételt eredményezhet. Már a standard könyvtárban is találhatunk példát: a vector, a vector< vector > és a map is ilyen. A list és a vector tároló ugyanolyan biztosítást ad az insert() egyelemû és többelemû változatához, mert azok megvalósítási módja azonos. A map viszont erõs biztosítást ad az egyelemû beszúráshoz, míg a többelemûhöz csak alapbiztosítást. Az egyelemû insert() a map esetében könnyen elkészíthetõ erõs biztosítással, a többelemû változat egyetlen logikus megvalósítási módja azonban a map esetében az, hogy az új elemeket egymás után szúrjuk be, és ehhez már nagyon nehéz lenne erõs garanciákat adni. A gondot itt az jelenti, hogy nincs egyszerû visszalépési lehetõség (nem tudunk korábbi sikeres beszúrásokat visszavonni), ha valamelyik elem beszúrása nem sikerül. Ha olyan többelemû beszúró mûveletre van szükségünk, amely erõs biztosítást ad, azaz vagy minden elemet hibátlanul beilleszt, vagy egyáltalán nem változtatja meg a tárolót, legegyszerûbben úgy valósíthatjuk meg, hogy egy teljesen új tárolót készítünk, majd ennek sikeres létrehozása után egy swap() mûveletet alkalmazunk: template void safe_insert(C& c, typename C::const_iterator i, Iter begin, Iter end) { C tmp(c.begin(),i); // az elöl levõ elemek másolása ideiglenes változóba copy(begin,end,inserter(tmp,tmp.end())); // új elemek másolása copy(i,c.end(),inserter(tmp,tmp.end())); // a záró elemek másolása swap(c,tmp); }
Szokás szerint, ez a függvény is hibásan viselkedhet, ha az elemek destruktora kivételt vált ki, ha viszont az elemek másoló konstruktora okoz hibát, a paraméterben megadott tároló változatlan marad.
E. Kivételbiztosság a standard könyvtárban
1293
E.4.3. A swap() A másoló konstruktorokhoz és értékadásokhoz hasonlóan a swap() eljárások is nagyon fontos szerepet játszanak sok szabványos algoritmusban és közvetlenül is gyakran használják a felhasználók. A sort() és a stable_sort() például általában a swap() segítségével rendezi át az elemeket. Tehát ha a swap() kivételt vált ki, miközben a tárolóban szereplõ értékeket cserélgeti, akkor a tároló elemei a csere helyett vagy változatlanok maradnak, vagy megkettõzõdnek. Vizsgáljuk meg a standard könyvtár swap() függvényének alábbi, egyszerû megvalósítását (§18.6.8): template void swap(T& a, T& b) { T tmp = a; a = b; b = tmp; }
Erre teljesül, hogy a swap() csak akkor eredményezhet kivételt, ha azt az elemek másoló konstruktora vagy másoló értékadása váltja ki. Az asszociatív tárolóktól eltekintve a szabványos tárolók biztosítják, hogy a swap() függvény ne váltson ki kivételeket. A tárolókban általában úgy is meg tudjuk valósítani a swap() függvényt, hogy csak az adatszerkezeteket cseréljük fel, melyek mutatóként szolgálnak a tényleges elemekhez (§13.5, §17.1.3). Mivel így magukat az elemeket nem kell mozgatnunk, azok konstruktorára vagy értékadó mûveletére nincs szükségünk, tehát azok nem kapnak lehetõséget kivétel kiváltására. Ezenkívül a szabvány biztosítja, hogy a könyvtár swap() függvénye nem tesz érvénytelenné egyetlen hivatkozást, mutatót és bejárót sem azok közül, melyek a felcserélt tárolók elemeire hivatkoznak. Ennek következtében kivételek egyetlen ponton léphetnek fel: az asszociatív tárolók összehasonlító objektumaiban, melyeket az adatszerkezet leírójának részeként kell másolnunk. Tehát az egyetlen kivétel, amit a szabványos tárolók swap() eljárása eredményezhet, az összehasonlító objektum másoló konstruktorából vagy értékadó mûveletébõl származik (§17.1.4.1). Szerencsére az összehasonlító objektumoknak általában annyira egyszerû másoló mûveleteik vannak, hogy nincs lehetõségük kivétel kiváltására. A felhasználói swap() függvények viszonylag egyszerûen nyújthatnak ugyanilyen biztosításokat, ha gondolunk rá, hogy mutatókkal ábrázolt adatok esetében elegendõ csak a mutatókat felcserélnünk, ahelyett, hogy lassan és precízen lemásolnánk a mutatók által kijelölt tényleges adatokat (§13.5, §16.3.9, §17.1.3).
1294
Függelékek és tárgymutató
E.4.4. A kezdeti értékadás és a bejárók Az elemek számára való memóriafoglalás és a memóriaterületek kezdeti értékadása alapvetõ része minden tárolónak (§E.3). Ebbõl következik, hogy a fel nem töltött (elõkészítetlen) memóriaterületen objektumot létrehozó szabványos eljárások az uninitialized_fill(), az uninitialized_fill_n() és az uninitialized_copy() (§19.4.4) semmiképpen sem hagyhatnak létrehozott objektumokat a memóriában, ha kivételt váltanak ki. Ezek az algoritmusok erõs biztosítást valósítanak meg (§E.2), amihez gyakran kell elemeket törölni, tehát az a követelmény, miszerint a destruktoroknak tilos kivételt kiváltaniuk, elengedhetetlen ezeknél a függvényeknél is (lásd §E.8[14]). Ezenkívül azoknak a bejáróknak is megfelelõen kell viselkedniük, melyeket paraméterként adunk át ezeknek az eljárásoknak. Tehát érvényes bejáróknak kell lenniük, érvényes sorozatokra kell hivatkozniuk, és a bejáró mûveleteknek (például a ++, a != vagy a * operátornak) nem szabad kivételt kiváltaniuk, ha érvényes bejárókra alkalmazzuk azokat. A bejárók (iterátorok) olyan objektumok, melyeket a szabványos algoritmusok és a szabványos tárolók mûveletei szabadon lemásolhatnak, tehát ezek másoló konstruktora és másoló értékadása nem eredményezhet kivételt. A szabvány garantálja, hogy a szabványos tárolók által visszaadott bejárók másoló konstruktora és másoló értékadása nem vált ki kivételt, így a vector::begin() által visszaadott bejárót például nyugodtan lemásolhatjuk, nem kell kivételtõl tartanunk. Figyeljünk rá, hogy a bejárókra alkalmazott ++ vagy -- mûvelet eredményezhet kivételt. Például egy istreambuf_iterator (§19.2.6) egy bemenethibát (logikusan) egy kivétel kiváltásával jelezhet, egy tartományellenõrzött bejáró pedig teljesen szabályosan jelezheti kivétellel azt, hogy megpróbáltunk kilépni a megengedett tartományból (§19.3). Akkor azonban nem eredményezhetnek kivételt, ha a bejárót úgy irányítjuk át egy sorozat egyik elemérõl a másikra, hogy közben a ++ vagy a -- egyetlen szabályát sem sértjük meg. Tehát az uninitialized_fill(), az uninitialized_fill_n() és az uninitialized_copy() feltételezi, hogy a bejárókra alkalmazott ++ és -- mûvelet nem okoz kivételt. Ha ezt mégis megteszik, a szabvány megfogalmazása szerint ezek nem is igazán bejárók, vagy az általuk megadott sorozat nem értelmezhetõ sorozatként. Most is igaz, hogy a szabvány nem képes megvédeni a felhasználót a saját maga által okozott nem meghatározható viselkedéstõl (§E.2).
E.4.5. Hivatkozások elemekre Ha elemre hivatkozó mutatót, referenciát vagy bejárót adunk át egy eljárásnak, az tönkreteheti a listát azzal, hogy az adott elemet érvénytelenné teszi:
E. Kivételbiztosság a standard könyvtárban
1295
void f(const X& x) { list lst; lst.push_back(x); list::iterator i = lst.begin(); *i = x; // x listába másolása // ... }
Ha az x változóban érvénytelen érték szerepel, a list destruktora nem képes hibátlanul megsemmisíteni az lst objektumot: struct X { int* p;
};
X() { p = new int; } ~X() { delete p; } // ...
void malicious() { X x; x.p = reinterpret_cast(7); f(x); }
// hibás x // idõzített bomba
Az f() végrehajtásának befejeztével meghívódik a list destruktora, amely viszont meghívja az X destruktorát egy érvénytelen értékre. Ha megpróbáljuk a delete p parancsot végrehajtani egy olyan p értékre, amely nem 0 és nem is létezõ X típusú értékre mutat, az eredmény nem meghatározható lesz és akár a rendszer azonnali összeomlását okozhatja. Egy másik lehetõség, hogy a memória érvénytelen állapotba kerül, ami sokkal késõbb, a program olyan részében okoz megmagyarázhatatlan hibákat, amely teljesen független a tényleges problémától. Ez a hibalehetõség nem gátolja meg a programozókat abban, hogy referenciákat és bejárókat használjanak a tárolók elemeinek kezelésére, hiszen mindenképpen ez az egyik legegyszerûbb és leghatékonyabb módszer az ilyen feladatok elvégzéséhez. Mindenesetre érdemes különösen elõvigyázatosnak lennünk a tárolók elemeire való hivatkozásokkal kapcsolatban. Ha egy tároló épsége veszélybe kerülhet, érdemes a kevésbé gyakorlott felhasználók számára biztonságosabb, ellenõrzött változatokat is készítenünk, például megadhatunk egy olyan eljárást, amely ellenõrzi, hogy az új elem érvényes-e, mielõtt beszúrja azt a fontos tárolóba. Természetesen ilyen ellenõrzéseket csak akkor végezhetünk, ha pontosan ismerjük a tárolóban tárolt elemek típusát.
1296
Függelékek és tárgymutató
Általában, ha egy tároló valamelyik eleme érvénytelenné válik, a tárolóra alkalmazott minden további mûvelet hibákat eredményezhet. Ez nem csak a tárolók sajátja: bármely objektum, amely valamilyen szempontból hibás állapotba kerül, a késõbbiekben bármikor okozhat problémákat.
E.4.6. Predikátumok Számos szabványos algoritmus és tároló használ olyan predikátumokat, melyeket a felhasználók adhatnak meg. Az asszociatív tárolók esetében ezek különösen fontos szerepet töltenek be: az elemek keresése és beszúrása is ezen alapul. A szabványos tárolók mûveletei által használt predikátumok is okozhatnak kivételeket, és ha ez bekövetkezik, a standard könyvtár mûveletei legalább alapbiztosítást nyújtanak, de sok esetben (például az egyelemû insert() mûveletnél) erõs biztosítás áll rendelkezésünkre (§E.4.1). Ha egy tárolóján végzett mûvelet közben egy predikátum kivételt vált ki, elképzelhetõ, hogy az ott tárolt elemek nem pontosan azok lesznek, amelyeket szeretnénk, de mindenképpen érvényes elemek. Például ha az == okoz kivételt a list::unique() (§17.2.2.3) mûvelet végrehajtása közben, nem várhatjuk el, hogy minden értékismétlõdés eltûnjön. A felhasználó mindössze annyit feltételezhet, hogy a listában szereplõ értékek érvényesek maradnak (lásd §E.5.3). Szerencsére a predikátumok ritkán csinálnak olyasmit, ami kivételt eredményezhet. Ennek ellenére a felhasználói szerkezetet kell használnunk. Ez a megoldás az egész programot rugalmasabbá teszi. Amikor új programot készítünk, lehetõségünk van arra, hogy átgondoltabb megközelítést találjunk és biztosítsuk, hogy erõforrásainkat olyan osztályokkal ábrázoljuk, melyek invariánsa alapbiztosítást nyújt (§E.2). Egy ilyen rendszerben lehetõség nyílik arra, hogy kiválasszuk a létfontosságú objektumokat és ezek mûveleteihez visszagörgetési módszereket alkalmazzunk (azaz erõs biztosítást adhatunk néhány egyedi feltétel mellett).
1302
Függelékek és tárgymutató
A legtöbb program tartalmaz olyan adatszerkezeteket és programrészeket, melyeket a kivételbiztosságra nem gondolva írtak meg. Ha szükség van rá, ezek a részek egy kivételbiztos keretbe ágyazhatók. Az egyik lehetõség, hogy biztosítjuk, hogy kivételek ne következzenek be (ez történt a C standard könyvtárával, §E.5.5), a másik megoldás pedig az, hogy felületosztályokat használunk, melyekben a kivételek viselkedése és az erõforrások kezelése pontosan meghatározható. Amikor olyan új típusokat tervezünk, amelyek kivételbiztos környezetben futnak majd, külön figyelmet kell szentelnünk azoknak az eljárásoknak, melyeket a standard könyvtár használni fog: a konstruktoroknak, a destruktoroknak, az értékadásoknak, összehasonlításoknak, swap függvényeknek, a predikátumként használt függvényeknek és a bejárókat kezelõ eljárásoknak. Ezt legkönnyebben úgy valósíthatjuk meg, hogy egy jó osztályinvariánst határozunk meg, amelyet minden konstruktor könnyedén biztosíthat. Néha úgy kell megterveznünk az osztályinvariánst, hogy az objektumoknak legyen egy olyan állapota, melyben egyszerûen törölhetõk, ha egy mûvelet kellemetlen helyen ütközik hibába. Ideális esetben ez az állapot nem egy mesterségesen megadott érték, amit csak a kivételkezelés miatt kellett bevezetni, hanem az osztály természetébõl következõ állapot (§E.3.5). Amikor kivételbiztossággal foglalkozunk, a fõ hangsúlyt az objektumok érvényes állapotainak (invariánsainak) meghatározására és az erõforrások megfelelõ felszabadítására kell helyeznünk. Ezért nagyon fontos, hogy az erõforrásokat közvetlenül osztályokkal ábrázoljuk. A vector_base (§E.3.2) ennek egyszerû példája. Az ilyen erõforrás-osztályok konstruktora alacsonyszintû erõforrásokat foglal le (például egy memóriatartományt a vector_base esetében), és invariánsokat állít be (például a mutatókat a megfelelõ helyekre állítja a vector_base osztályban). Ezen osztályok destruktora egyszerûen felszabadítja a lefoglalt erõforrást. A részleges létrehozás szabályai (§14.4.1) és a kezdeti értékadás az erõforrás lefoglalásával módszer (§14.4) alkalmazása lehetõvé teszi, hogy az erõforrásokat így kezeljük. Egy jól megírt konstruktor minden objektum esetében beállítja a megfelelõ invariánst (§24.3.7.1), tehát a konstruktor olyan értéket ad az objektumnak, amely lehetõvé teszi, hogy a további mûveleteket egyszerûen meg tudjuk írni és sikeresen végre tudjuk hajtani. Ebbõl következik, hogy a konstruktoroknak gyakran kell erõforrást lefoglalniuk. Ha ezt nem tudják elvégezni, kivételt válthatnak ki, így az objektum létrehozása elõtt foglalkozhatunk a jelentkezõ problémákkal. Ezt a megközelítést a nyelv és a standard könyvtár közvetlenül támogatja (§E.3.5). Az a követelmény, hogy az erõforrásokat fel kell szabadítanunk és az operandusokat érvényes állapotban kell hagynunk a kivétel kiváltása elõtt, azt jelenti, hogy a kivételkezelés terheit megosztjuk a kivételt kiváltó függvény, a hívási láncban levõ függvények és a kivételt ténylegesen kezelõ eljárás között. Egy kivétel kiváltása nem azt a hibakezelési stílust jelen-
E. Kivételbiztosság a standard könyvtárban
1303
ti, hogy hagyjuk az egészet valaki másra. Minden függvénynek, amely kivételt vált ki vagy ad tovább, kötelessége felszabadítani azokat az erõforrásokat, melyek hatáskörébe tartoznak, operandusait pedig megfelelõ értékre kell állítania. Ha az eljárások ezt a feladatot nem képesek végrehajtani, a kivételkezelõ nemigen tehet mást, minthogy megpróbálja szépen befejezni a program mûködését.
E.7. Tanácsok [1] Legyünk tisztában azzal, milyen szintû kivételbiztosságra van szükségünk. §E.2. [2] A kivételbiztosságnak egy teljes körû hibatûrési stratégia részének kell lennie. §E.2. [3] Az alapbiztosítást minden osztályhoz érdemes megvalósítani, azaz az invariánsokat mindig tartsuk meg és az erõforrás-lyukakat mindig kerüljük el. §E.2, §E.3.2, §E.4. [4] Ahol lehetõség és szükség van rá, valósítsunk meg erõs biztosítást, azaz egy mûvelet vagy sikeresen hajtódjon végre, vagy minden operandusát hagyja változatlanul. §E.2, §E.3. [5] Destruktorokban ne fordulhasson elõ kivétel. §E.2, §E.3.2, §E.4. [6] Ne váltson ki kivételt egy érvényes sorozatban mozgó bejáró. §E.4.1, §E.4.4. [7] A kivételbiztosság foglalja magában az önálló mûveletek alapos vizsgálatát. §E.3. [8] A sablon osztályokat úgy tervezzük meg, hogy azok átlátszóak legyenek a kivételek számára. §E.3.1. [9] Az init() függvény helyett használjunk konstruktort az erõforrások lefoglalásához. §E.3.5. [10] Adjunk meg invariánst minden osztályhoz, hogy ezzel pontosan meghatározzuk érvényes állapotaikat. §E.2, §E.6. [11] Gyõzõdjünk meg róla, hogy objektumaink mindig érvényes állapotba állíthatók anélkül, hogy kivételektõl kellene tartanunk. §E.3.2, §E.6. [12] Az invariánsok mindig legyenek egyszerûek. §E.3.5. [13] Kivétel kiváltása elõtt minden objektumot állítsunk érvényes állapotba. §E.2, §E.6. [14] Kerüljük el az erõforrás-lyukakat. §E.2, §E.3.1, §E.6. [15] Az erõforrásokat közvetlenül ábrázoljuk. §E.3.2, §E.6. [16] Gondoljunk rá, hogy a swap() függvény gyakran használható az elemek másolása helyett. §E.3.3.
1304
Függelékek és tárgymutató
[17] Ha lehetõség van rá, a try blokkok használata helyett a mûveletek sorrendjének jó megválasztásával kezeljük a problémákat. §E.3.4. [18] Ne töröljük a régi információkat addig, amíg a helyettesítõ adatok nem válnak biztonságosan elérhetõvé. §E.3.3, §E.6. [19] Használjuk a kezdeti értékadás az erõforrás megszerzésével módszert. §E.3, §E.3.2, §E.6. [20] Vizsgáljuk meg, hogy asszociatív tárolóinkban az összehasonlító mûveletek másolhatók-e. §E.3.3. [21] Keressük meg a létfontosságú adatszerkezeteket és ezekhez adjunk meg olyan mûveleteket, melyek erõs biztosítást adnak. §E.6.
E.8. Gyakorlatok 1. (*1) Soroljuk fel az összes kivételt, amely elõfordulhat az §E.1 pont f() függvényében. 2. (*1) Válaszoljunk az §E.1 pontban, a példa után szereplõ kérdésekre. 3. (*1) Készítsünk egy Tester osztályt, amely idõnként a legalapvetõbb mûveletekben okoz kivételt, például a másoló konstruktorban. A Tester osztály segítségével próbáljuk ki saját standard könyvtárunk tárolóit. 4. (*1) Keressük meg a hibát az §E.3.1 pontban szereplõ vector konstruktorának rendezetlen változatában és írjunk programot, amely tönkreteszi az osztályt. Ajánlás: elõször írjuk meg a vector destruktorát. 5. (*2) Készítsünk egyszerû listát, amely alapbiztosítást nyújt. Állapítsuk meg nagyon pontosan, milyen követelményeket kell a felhasználónak teljesítenie a biztosítás megvalósításához. 6. (*3) Készítsünk egyszerû listát, amely erõs biztosítást nyújt. Alaposan ellenõrizzük az osztály mûködését. Indokoljuk meg, miért tartjuk ezt a megoldást biztonságosabbnak. 7. (*2.5)Írjuk újra a §11.12 String osztályát úgy, hogy ugyanolyan biztonságos legyen, mint a szabványos tárolók. 8. (*2) Hasonlítsuk össze a vector osztályban meghatározott értékadás és a safe_assign() függvény különbözõ változatait a futási idõ szempontjából. (§E.3.3) 9. (*1.5) Másoljunk le egy memóriafoglalót az értékadó operátor használata nélkül (hiszen az operator=() megvalósításához erre van szükségünk az §E.3.3 pontban).
E. Kivételbiztosság a standard könyvtárban
1305
10. (*2) Írjunk a vector osztályhoz alapbiztosítással egy egyelemû és egy többelemû erase(), illetve insert() függvényt.§E.3.2. 11. (*2) Írjunk a vector osztályhoz erõs biztosítással egy egyelemû és egy többelemû erase(), illetve insert() függvényt (§E.3.2). Hasonlítsuk össze ezen függvények költségét és bonyolultságát az elõzõ feladatban szereplõ függvényekével. 12. (*2) Készítsünk egy safe_insert() függvényt (§E.4.2), amely egy létezõ vector objektumba szúr be elemet (nem pedig egy ideiglenes változót másol le). Milyen kikötéseket kell tennünk a mûveletekre? 13. (*2.5) Hasonlítsuk össze méret, bonyolultság és hatékonyság szempontjából a 12. és a 13. feladatban szereplõ safe_insert() függvényt az §E.4.2 pontban bemutatott safe_insert() függvénnyel. 14. (*2.5) Írjunk egy jobb (gyorsabb és egyszerûbb) safe_insert() függvényt, kifejezetten asszociatív tárolókhoz. Használjuk a traits eljárást egy olyan safe_insert() megvalósításához, amely automatikusan kiválasztja az adott tárolóhoz optimális megvalósítást. Ajánlás: §19.2.3. 15. (*2.5) Próbáljuk megírni az uninitialized_fill() függvényt (§19.4.4, §E.3.1) úgy, hogy az megfelelõen kezelje a kivételeket kiváltó destruktorokat is. Lehetséges ez? Ha igen, milyen áron? Ha nem, miért nem? 16. (*2.5) Keressünk egy tárolót egy olyan könyvtárban, amely nem tartozik a szabványhoz. Nézzük át dokumentációját és állapítsuk meg, milyen kivételbiztossági lehetõségek állnak rendelkezésünkre. Végezzünk néhány tesztet, hogy megállapítsuk, mennyire rugalmas a tároló a memóriafoglalásból vagy a felhasználó által megadott programrészekbõl származó kivételekkel szemben. Hasonlítsuk össze a tapasztaltakat a standard könyvtár megfelelõ tárolójának szolgáltatásaival. 17. (*3) Próbáljuk optimalizálni az §E.3 pontban szereplõ vector osztályt a kivételek lehetõségének figyelmen kívül hagyásával. Például töröljünk minden try blokkot. Hasonlítsuk össze az így kapott változat hatékonyságát a standard könyvtár vector osztályának hatékonyságával. Hasonlítsuk össze a két változatot méret és bonyolultság szempontjából is. 18. (*1) Adjunk meg invariánst a vector osztály (§E.3) számára úgy, hogy megengedjük, illetve megtiltjuk a v==0 esetet (§E.3.5). 19. (*2.5) Nézzük végig egy vector osztály megvalósításának forráskódját. Milyen biztosítás áll rendelkezésünkre az értékadásban, a többelemû insert() utasításban és a resize() függvényben? 20. (*3) Írjuk meg a hash_map (§17.6) olyan változatát, amely ugyanolyan biztonságos, mint a szabványos tárolók.