Generatı v programok helyesse ge

Hasonló dokumentumok
C++ Standard Template Library (STL)

Fejlett programozási nyelvek C++ Iterátorok

Challenge Accepted:C++ Standard Template Library

Generatív programok helyessége

1. Template (sablon) 1.1. Függvénysablon Függvénysablon példányosítás Osztálysablon

1. Mi a fejállományok szerepe C és C++ nyelvben és hogyan használjuk őket? 2. Milyen alapvető változókat használhatunk a C és C++ nyelvben?

Az alábbi példában a Foo f(5); konstruktor hívása után mennyi lesz f.b értéke? struct Foo { int a, b; Foo(int c):a(c*2),b(c*3) {} };

1) Hány byte-on tárol a C++ egy karaktert (char)? implementáció-függő ( viszont lásd 79. megjegyzés ) 1 8 4

Generikus Típusok, Kollekciók

Már megismert fogalmak áttekintése

OOP #14 (referencia-elv)

GENERIKUS PROGRAMOZÁS Osztálysablonok, Általános felépítésű függvények, Függvénynevek túlterhelése és. Függvénysablonok

Alprogramok, paraméterátadás

STL. Algoritmus. Iterátor. Tároló. Elsődleges komponensek: Tárolók Algoritmusok Bejárók

STL gyakorlat C++ Izsó Tamás május 9. Izsó Tamás STL gyakorlat/ 1

Programozási technológia

Programozási nyelvek Java

1.AA MEGOLDÓ BERCI AA 1.

Interfészek. PPT 2007/2008 tavasz.

500. AA Megoldó Alfréd AA 500.

Bevezetés a programozásba 2

GTL Graphical Template Library

C++ programozási nyelv

Programozási Nyelvek: C++

Generikus osztályok, gyűjtemények és algoritmusok

1. Bevezetés A C++ nem objektumorientált újdonságai 3

ISA szimulátor objektum-orientált modell (C++)

OpenCL alapú eszközök verifikációja és validációja a gyakorlatban

Bánsághi Anna 2014 Bánsághi Anna 1 of 33

C++ referencia. Izsó Tamás február 17. A C++ nyelvben nagyon sok félreértés van a referenciával kapcsolatban. A Legyakoribb hibák:

OOP. Alapelvek Elek Tibor

Pelda öröklődésre: import java.io.*; import java.text.*; import java.util.*; import extra.*;

mul : S T N 1 ha t S mul(s, t) := 0 egyébként Keresés Ezt az eljárást a publikus m veletek lenti megvalósításánál használjuk.

Programozás módszertan

C++ programozási nyelv Konstruktorok-destruktorok

Bevezetés a C++ programozási nyelvbe

Statikus adattagok. Statikus adattag inicializálása. Speciális adattagok és tagfüggvények. Általános Informatikai Tanszék

Programozás alapjai C nyelv 8. gyakorlat. Mutatók és címek (ism.) Indirekció (ism)

C vagy C++? Programozási Nyelvek és Fordítóprogramok Tanszék. Pataki Norbert. Programozási Nyelvek I.

Bevezetés a programozásba Előadás: Tagfüggvények, osztály, objektum

Osztályok. 4. gyakorlat

1000.AA Megoldo Alfréd 1000.A

Smart Pointer koncepciója

Programozási nyelvek (ADA)

Programozási alapismeretek 4.

Algoritmusok és adatszerkezetek gyakorlat 06 Adatszerkezetek

Mutatók és címek (ism.) Programozás alapjai C nyelv 8. gyakorlat. Indirekció (ism) Néhány dolog érthetőbb (ism.) Változók a memóriában

3. Osztályok II. Programozás II

Objektumelvű programozás

Bevezetés, a C++ osztályok. Pere László

500.AA Megoldó Kulcsár 500.A

Objektum orientált kiterjesztés A+ programozási nyelvhez

A szemantikus elemzés helye. A szemantikus elemzés feladatai. A szemantikus elemzés feladatai. Deklarációk és láthatósági szabályok

Bevezetés a programozásba II. 5. Előadás: Másoló konstruktor, túlterhelés, operátorok

JUnit. JUnit használata. IDE támogatás. Parancssori használat. Teszt készítése. Teszt készítése

C++ programozási nyelv

C++11 TÓTH BERTALAN C++ PROGRAMOZÁS STL KONTÉNEREKKEL

500. CC Megoldó Alfréd CC 500.

Mutatók és mutató-aritmetika C-ben március 19.

List<String> l1 = new ArrayList<String>(); List<Object> l2 = l1; // error

Programozás II. 3. gyakorlat Objektum Orientáltság C++-ban

Collections. Összetett adatstruktúrák

Virtuális függvények (late binding)

Osztálytervezés és implementációs ajánlások

Felhasználó által definiált adattípus

Osztálytervezés és implementációs ajánlások

Számítógép és programozás 2

Programozás II gyakorlat. 8. Operátor túlterhelés

C++ Gyakorlat jegyzet 10. óra.

C++ Standard Template Library

Objektumok inicializálása

és az instanceof operátor

.AA Megoldó Alfréd AA.

Java VIII. Az interfacei. és az instanceof operátor. Az interfészről általában. Interfészek JAVA-ban. Krizsán Zoltán

Globális operátor overloading

A TANTÁRGY ADATLAPJA

Eseménykezelés. Szoftvertervezés és -fejlesztés II. előadás. Szénási Sándor.

Bevezetés a programozásba Előadás: Objektumszintű és osztályszintű elemek, hibakezelés

C# osztálydeníció. Krizsán Zoltán 1. .net C# technológiák tananyag objektum orientált programozás tananyag

PROGRAMOZÁS tantárgy. Gregorics Tibor egyetemi docens ELTE Informatikai Kar

500.AA Megoldo Arisztid 500.A

Programozás I. 3. gyakorlat. Szegedi Tudományegyetem Természettudományi és Informatikai Kar

Programozás II. 4. Dr. Iványi Péter

0. Megoldó Manó 0. Programozás alapjai 2. (inf.) pót zárthelyi gyak. hiányzás: 2 n/kzhp: n/11,5. ABCDEF IB.028/2.

.Net adatstruktúrák. Készítette: Major Péter

Programozási nyelvek Java

Pénzügyi algoritmusok

Pénzügyi algoritmusok

Bevezetés a Programozásba II 12. előadás. Adatszerkezetek alkalmazása (Standard Template Library)

Kivételkezelés, beágyazott osztályok. Nyolcadik gyakorlat

503.AA Megoldo Arisztid 503.A

Programozás II. 2. Dr. Iványi Péter

8. gyakorlat Pointerek, dinamikus memóriakezelés

Programozás C++ -ban 2007/7

Adatszerkezetek 2. Dr. Iványi Péter

500. DD Megoldó Alfréd DD 500.

Memóriakezelés, dinamikus memóriakezelés

Járműfedélzeti rendszerek II. 3. előadás Dr. Bécsi Tamás

C# Nyelvi Elemei. Tóth Zsolt. Miskolci Egyetem. Tóth Zsolt (Miskolci Egyetem) C# Nyelvi Elemei / 18

Programozás II gyakorlat. 6. Polimorfizmus

Átírás:

Generatı v programok helyesse ge Doktori e rtekeze s 2013 Pataki Norbert patakino@elte.hu Te mavezeto : Dr. Porkola b Zolta n, egyetemi docens Eo tvo s Lora nd Tudoma nyegyetem, Informatikai Kar, 1117 Budapest, Pa zma ny Pe ter se ta ny 1/C ELTE IK Doktori Iskola Doktori program: Az informatika alapjai e s mo dszertana Az iskola vezeto je: Dr. Benczu r Andra s A program vezeto je: Dr. Demetrovics Ja nos akade mikus A projekt az Euro pai Unio ta mogata sa val, az Euro pai Szocia lis Alap ta rsfinanszı roza sa val valo sul meg (a ta mogata s sza ma TA MOP4.2.1./B-09/1/KMR-2010-0003).

Tartalomjegyzék I. Bevezetés 5 I.1. Célkitűzések........................... 6 I.2. A dolgozat felépítése...................... 7 II. Alapok 9 II.1. Sablonok C++-ban....................... 9 II.2. Generatív és generikus programozás.............. 13 II.3. A C++ Standard Template Library.............. 14 II.4. Motivációs példák........................ 24 II.4.1. Fordítási hibaüzenetek................. 24 II.4.2. Invalid iterátorok.................... 27 II.4.3. Funktorokkal kapcsolatos hibák............ 29 II.4.4. Allokátorokkal kapcsolatos hibák........... 35 II.4.5. Másoló algoritmusokkal kapcsolatos hibák...... 36 II.4.6. Törlő algoritmusokkal kapcsolatos hibák....... 38 II.4.7. A unique algoritmus.................. 39 II.4.8. Algoritmusok speciális előfeltételei........... 40 II.4.9. A find és a count algoritmus............. 41 II.4.10. A vector<bool> konténer............... 42 II.4.11. COAP.......................... 45 II.4.12. Fejállományokkal kapcsolatos problémák....... 47 II.4.13. Iterátorok konverziója................. 47 II.4.14. Az asszociatív konténerek hordozhatósággal kapcsolatos problémái..................... 49 II.4.15. A vector és a string reallokációja.......... 50 II.4.16. Iterátorok és pointerek összetévesztése........ 51 II.4.17. Virtuális destruktorok hiánya............. 52 III. Az STL formális megközelítése 54 III.1. A Hoare-módszer bővítése................... 54 III.1.1. A Hoare-módszer.................... 54 2

Tartalomjegyzék 3 III.1.2. A formalizmus bővítése................. 58 III.1.3. Specifikációk...................... 60 III.1.4. Példák.......................... 63 III.2. LaCert.............................. 69 III.3. Összegzés............................ 74 IV. Fordítás idejű megoldások 75 IV.1. Warning-ok generálása..................... 75 IV.2. Hibás példányosítások...................... 78 IV.2.1. A vector<bool> konténer............... 78 IV.2.2. COAP.......................... 81 IV.3. Algoritmusok.......................... 82 IV.3.1. Az iterator traits kibővítése............ 82 IV.3.2. Másoló algoritmusok.................. 83 IV.3.3. A count és a find algoritmus............. 87 IV.3.4. A unique algoritmus.................. 90 IV.4. Adaptálható funktorok..................... 94 IV.5. Allokátorok........................... 96 IV.6. Reverse iterátorok........................ 97 IV.7. Lusta példányosítás....................... 99 IV.8. Összegzés............................ 100 V. Futási idejű megoldások 102 V.1. Az iterator traits kibővítése................ 102 V.2. Invalid iterátorok........................ 103 V.3. Másolás-biztonságos iterátorok................. 107 V.4. Törlő iterátorok......................... 109 V.5. Algoritmusok előfeltétele.................... 111 V.6. Funktorok............................ 114 V.7. Összegzés............................ 116 VI. Összefoglalás 118 A. Az STL bővítése a C++11-ben 132 A.1. Konténerek........................... 132 A.2. Algoritmusok.......................... 132 A.3. Iterátorok............................ 133

Köszönetnyilvánítás Legelőször szeretném témavezetőmnek, Porkoláb Zoltánnak (gsd-nek) megköszönni a sokéves témavezetői munkáját! A közös munkánk nagyon sokat jelent nekem. Még a unique-ot is legyőztük! Köszönöm páromnak, Melindának, hogy megteremtette az otthonunkat, ahol kényelmesen dolgozhattam, támogatott és gondosan lektorálta a cikkeket és a disszertációmat. Még nagyon hosszú a lista, nem szeretnék senkit sem kihagyni, de szeretném megköszönni a családomnak, a barátaimnak és a munkatársaimnak a kitartó támogatást. A szerzőtársaimnak köszönöm a munkát, amit a közös cikkekbe öltek. 4

I. fejezet Bevezetés A programozás történetének elmúlt ötven évében egyre bonyolultabb és öszszetettebb alkalmazások születtek. Ahogy a szoftverek egyre komplexebbé váltak, a fejlesztőknek egyre több implementációs részlettel kellett foglalkozniuk. Felmerült az igény arra, hogy a rendszeresen használt kódrészleteket ne kelljen újra és újra megírni, hanem azok külső egységekként átvihetők legyenek akár különböző alkalmazások között is. Az ilyen elven megvalósított szoftver egységeket nevezzük könyvtáraknak (library). Kezdetben, a procedurális paradigma szemléletének megfelelően, a függvénykönyvtárak terjedtek el. Alprogramok (függvények, eljárások) segítségével előre megírtak olyan funkcionalitásokat, amelyeket később különböző paraméter értékekkel számos környezetben meghívhattak. Például FORTRANhoz vagy C-hez számtalan ilyen elven működő könyvtár elérhető. Az objektum-orientált programozás térhódításával együtt a könyvtárak felépítése is megváltozott. Függvénykönyvtárak helyett osztálykönyvtárakat implementáltak és használtak a programozók. Ekkor előre megírt osztályok, öröklődés és virtuális metódusok segítségével hoztak létre osztályhierarchiákon alapuló könyvtárakat, amelyek a korábbi megoldásokhoz képest jobban támogatták a kódújrafelhasználást. A Simula, Smalltalk, Eiffel és a Java programozási nyelvek elterjedésével ezek a könyvtárak széleskörben elfogadottá váltak. Az objektum-orientált könyvtáraknál még flexibilisebb megoldást nyújtanak a generikus könyvtárak. A konténerek és az algoritmusok függetlensége miatt ezek a rendszerek egyszerre több irányba bővíthetőek és feloldanak olyan problémákat, amelyeket régebbi megközelítéssel nem lehet kényelmesen kezelni. A C++ sablonok segítségével fordítási időben végrehajtódó kódokat is lehet írni, ezek a template metaprogramok. Léteznek metaprogramozási technikákon alapuló könyvtárak is [1, 104, 118]. Az aktív könyvtárak (active 5

6 Bevezetés libraries) olyan könyvtárak, amelyek fordítási időben dinamikusan viselkednek: döntéseket hoznak a felhasználás környezetének ismeretében, optimalizációkat végeznek fordítás közben, stb. [15]. A C++ Standard Template Library (STL) a generikus programozási paradigmán alapuló könyvtárak mintapéldája. Professzionális C++ program elképzelhetetlen a szabványkönyvtár részét képező STL alkalmazása nélkül. Az elegáns kialakítású könyvtár használata csökkenti a klasszikus C és C++ hibák lehetőségét, növeli a kód minőségét, karbantarthatóságát, érthetőségét és hatékonyságát [78]. Ugyanakkor a könyvtár alkalmazása nem garantál hibamentes kódot, sőt a könyvtár generikus megközelítése miatt új típusú hibalehetőségek keletkezhetnek. Ezeknek egy részét a fordítóprogram nem ellenőrzi és futási időben nem derül ki a kód hibás jellege. Bizonyos esetekben a hiba okát is nehéz felderíteni akár debugger alkalmazások segítségével is. Nem megdöbbentő, hogy ilyen hibák nagy számmal előfordulnak C++ nyelven írt programok implementációjában [53]. Kutatásaim középpontjában ezen hibalehetőségek leküzdése áll mégpedig úgy, hogy az STL hatékonysága és rugalmassága megmaradjon. I.1. Célkitűzések Kutatásaim kiindulópontja a [53] könyv volt, melyben 50 tanács található az STL helyes, hatékony használatáról. Ezek a tanácsok szövegesen (informálisan) írták le, hogy mit hogyan érdemes használni, mi milyen hibát okozhat. Többek között ilyen témák találhatók a könyvben: Részesítsük előnyben az intervallumokat használó tagfüggvényeket Használjuk az empty-t, a size == 0 vizsgálat helyett Használjuk a reserve-t, hogy elkerüljük a felesleges reallokációkat Fontoljuk meg az asszociatív tárolók cseréjét rendezett vector-ral Ismerjük a lehetőségeinket a rendezésekkel kapcsolatban Szoftveres megoldást nem kínál a könyv a tanácsokhoz, ezért azt a célt tűztem ki magam elé, hogy a programozók dolgát megkönnyítem az elkövethető hibák minél átfogóbb kiszűrésével. A szűrést segítsem mind formális, mind szoftveres eszközökkel. Egy kísérleti eszköz az STLlint [35], mely egy módosított fordítóprogram alapján működik, sokáig online elérhető volt,

Bevezetés 7 de működése nem váltotta be a hozzá fűzött reményeket, támogatása megszűnt. Az STLlint kizárólag fordítási idejű információk alapján működött. Az én megoldásaim ezzel szemben a könyvtár implementációjának bővítésén, változtatásán alapulnak, szabványos fordítóprogramok használata mellett. Én is igyekeztem a lehetséges hibákat fordítási időben felderíteni és a C++ sablon konstrukciója segítségével fordítási figyelmeztetéseket generálni, de a megoldásaim egy része futási időben működik. Tehát céljaimat a következő prioritással lehet definiálni: 1. Az STL generikusságából adódó hibalehetőségek kiszűrése fordítási időben, nem-intruzív módon. 2. Az STL generikusságából adódó hibalehetőségek kiszűrése fordítási időben, az STL implementáció módosításával. 3. Az STL generikusságából adódó hibalehetőségek kiszűrése futási időben, nem-intuzív módon, a szabványos aszimptotikus futási idők betartásával (törekedve a minimális overhead-re). 4. Az STL generikusságából adódó hibalehetőségek kiszűrése futási időben, az STL implementáció módosításával, a szabványos aszimptotikus futási idők betartásával (törekedve a minimális overhead-re). 5. Az STL generikusságából adódó hibalehetőségek kiszűrése futási időben, nem-intruzív módon, a szabvány aszimptotikus futási idő korlátainak megsértésével. 6. Az STL generikusságából adódó hibalehetőségek kiszűrése futási időben, az STL implementáció módosításával, a szabvány aszimptotikus futási idő korlátainak megsértésével. Emellett törekedtem a reverse-kompatibilitásra: meglévő (hibás) kódrészletek (legacy kódok) működhessenek az eredeti viselkedésnek megfelelően, hiszen nem lehet több millió kódsort hirtelen átírni például eddig nem ismert kivételek elkapására. Ha nem maradnék reverse-kompatibilis és például kivételeket dobnék hiba esetén, akkor rengeteg program abortálhatna le nem kezelt kivételek miatt. I.2. A dolgozat felépítése A dolgozat további fejezeteiben bemutatom a kutatásaimat, amelyekkel az STL használata biztonságosabbá tehető. A második fejezetben bemutatom a

8 Bevezetés generatív és generikus programozási paradigmát, részletezem az STL felépítését és a fontosabb részeit. Emellett példákat adok olyan kódokra, amelyek lefordulnak és hibás voltukat semmi sem fedi fel. A harmadik fejezetben bemutatom az általam kidolgozott eszközöket, amelyekkel az STL formálisan definiálható. A negyedik fejezetben olyan megoldásokat adok, amelyek fordítási időben elősegítik az STL hibás használatának kiszűrését a fordítóprogram módosítása nélkül. Az ötödik fejezetben olyan általam kidolgozott eszközöket részletezek, amelyek vagy futási időben jelzik az STL hibás használatát vagy leküzdik a hiba okát. Végezetül összefoglalom a dolgozat eredményeit. Kutatásaimat a 2003-as C++ szabvány szerint végeztem. Az azóta elfogadott C++11 szabvány kapcsolódó részeit a függelékben ismertetem.

II. fejezet Alapok II.1. Sablonok C++-ban A CLU programozási nyelv vezette be először azt a nyelvi konstrukciót, melylyel típussal paraméterezhetünk programegységeket. A parametrikus polimorfizmus legfontosabb eszköze lett a template vagy generic [115]. A C++ template-jei segítségével osztály- és függvénysablonok írhatóak, amelyek sablonparaméterekkel paraméterezhetőek: fordítási időben ismert értékű paraméterekkel láthatóak el. Ezen paraméterek ismeretében a fordítóprogram (compiler) képes példányosítani a sablont és generálni a konkrét függvényt vagy osztályt. Vizsgáljuk meg a következő függvénysablon példát: template <class T> const T& max( const T& a, const T& b ) return a < b? b : a; A kódrészlet két tetszőleges, de azonos típusú objektum közül adja viszsza a nagyobbat. Használható int-ekkel, double-ökkel, stb. minden olyan típussal lehet ezt a sablont használni, amelynek van operator< művelete. Látható, hogy ez az elvárás csak a sablon törzséből derül ki. A sablon önmagában nem használható, a fordítóprogram sem elemzi a kódot átfogóan, nem generálódik belőle alacsony-szintű kód. Példányosítani (instantiate) kell a használathoz. Példányosításkor a sablonból konkrét kód generálódik, amelyben a formális sablonparaméterek helyén az aktuális paraméterek szerepelnek. C++-ban a fordítóprogram képes a függvénysablonok esetében a hívás 9

10 Alapok paramétereiből levezetni a sablon paramétereket, ezt a nevezik paraméterdedukciónak (parameter deduction). Az általánosságnak hátránya is van, például nem definiált, hogy mit ír ki az alábbi kódrészlet: std::cout << max( "one", "ten" ); Ilyenkor a fordítóprogram a "one" és a "ten" literálok típusát egyaránt const char[4]-nek vezeti le. A tömbök konvertálódnak első elemre mutató pointerré, és a kódrészlet két független tömb első elemére mutató pointert hasonlít össze, melynek eredménye nemdefiniált. Ha lexikografikus rendezést szeretnénk használni, akkor a függvénysablon explicit specializációját alkalmazhatjuk: std::cout << max<std::string>( "one", "ten" ); A C++ osztálysablonok esetében lehetőséget ad felhasználói specializációra is [111]. A specializációnak két fajtája van: részleges és teljes. A részleges specializáció esetében egy eltérő implementáció léphet életbe, ha egy típus csoport valamely tagjával példányosítjuk a sablont. A teljes specializáció esetében egy eltérő implementáció léphet életbe, ha egy konkrét típussal példányosítjuk a sablont. Nézzük meg az alábbi példát: 1 template <class T> 2 class Foo 3 4 //... 5 ; 6 7 template<class T> 8 class Foo<T*> 9 10 //... 11 ; 12 13 template <> 14 class Foo<bool> 15 16 //... 17 ; 18 19 Foo<char> a; // 1-5 sorok példányosítása

Alapok 11 20 Foo<bool*> b; // 7-11 sorok példányosítása 21 Foo<bool> c; // 13-17 sorok példányosítása A különböző sablonparaméterek miatt eltérő implementációt használnak a különböző objektumok. A specializációk más belső reprezentációt használhatnak, sőt a publikus interface-ük is eltérő lehet. Mivel a sablon paraméterek fordítás idejű paraméterek, így az, hogy melyik specializációt alkalmazzuk fordítási időben kiderül. A sablonok specializációi egy újfajta megközelítést hoztak a C++ nyelvbe. Segítségükkel fordítási időben futó kódrészletek hajthatóak végre, ezeket nevezzük template metaprogramoknak [1]. A metaprogramokat funkcionális megközelítéssel kell megírni: csak a rekurzív példányosításokra és specializációkra számíthatnak a programozók, nincsenek más vezérlési szerkezetek. A metaprogramok résznyelve Turing-teljes, de a valódi korlátai a mai napig kutatott terület [84]. A metaprogramok tipikus alkalmazásai: extra fordítás-idejű ellenőrzések, algoritmusok végrehajtása, a futás-idejű programok optimalizációi, domain-specifikus nyelvek definiálása. A metaprogramok alanya maga a C++ program. A metaprogramok lehetősége a C++ sablonjait egy rendkívül fontos konstrukcióvá teszi. A max sablon függvénynél már említettem, hogy a sablonparaméterekkel kapcsolatos elvárások kizárólag az implementációban jelennek meg, a deklarációban semmilyen információ nem szerepel ezzel kapcsolatban. Ha olyan típussal használjuk, amelynek nincs operator< művelete (pl. komplex számok kezeléséhez használt std::complex<double>), akkor a fordítóprogram példányosítja a sablont és példányosított kód fordításakor realizálja, hogy a kód nem lefordítható és hibaüzenet ad, amelyben a sablonra hivatkozik. A fordítóprogramok jelzik, hogy milyen aktuális sablonparaméterek mellett jött elő a hiba, de nem a példányosítást jelzik a hiba okaként. Ennek a jelenségnek az az oka, hogy a C++ sablonjai megszorítás nélküliek (unconstrained). Ez tervezési tulajdonsága a C++ sablonjainak [26]. Ez a tervezési tulajdonság biztonságos abból a szempontból, hogy hibás példányosítás esetében fordítási hibaüzenetet kap a programozó, nem jön létre hibásan futó program. Ugyanakkor sokszor bonyolult, nehezen érthető hibaüzenetekkel jár a megszorítás nélküli sablonok hibás használata. Ezért a kutatók elkezdtek metaprogramozási alapokkal ellátott könyvtárakat implementálni, amivel a fordítás korábbi pontján kiderülnek a hibás példányosítások. Sajnos azonban ezek a könyvtárak nem tudnak minden problémát kezelni, ezért a kutatók nyelvi bővítést szorgalmaztak a könyvtár-alapú megoldásokkal szemben [31]. Az új C++ nyelvi konstrukciót, a concept-et két eltérő formában képzelték el: a University of Texas A&M [95] és az Indiana University [39]

12 Alapok kutatói. A konstrukciók közös lényege, hogy típusmegszorítások definiálhatóak legyenek a C++ sablonparaméterein, oly módon, hogy megtartsák a C++ sablon rendszerének előnyeit. A két eltérő verziót egységessé formálták [33], implementációs technikákat dolgoztak ki [34, 37, 38] és egy kísérleti fordítóprogramot (ConceptC++) implementáltak, hogy gyakorlatban is ki lehessen próbálni az ötleteket. A concept lett a leginkább várt nyelvi bővítés az új C++ szabványban, miután 2008-ban beszavazták a C++0x-be. Stroustrup egy eltérő, egyszerűsített concept fogalmat definiált [94], aminek az lett a következménye, hogy 2009 nyarán a Szabványosítási Bizottság úgy döntött, hogy az új C++ szabványban mégsem lesz benne a concept konstrukció. Nem csak típusok lehetnek sablonparaméterek. C++-ban integrális konstansok (pl. int-ek, bool-ok, char-ok, stb.), mint fordítási idejű adatok, szintén átadhatóak sablonparaméterként. Osztálysablonok esetében lehetőség van default sablonparaméterek megadására is. Az ilyen paramétereket objektumok típusának megadásakor nem kötelező megadni, ha nincs megadva, akkor a default paraméterek lépnek életbe. A sablonok alkalmazásával nagymértékben növelhető a programok biztonsága. A dolgozatban részletesebben tárgyalt témák mellett az alábbi kutatásokat folytattam C++ template-kkel kapcsolatban. Megvizsgáltam egy template metaprogramozás alapú tesztelési keretrendszer lehetőségét [62]. A cikkben megmutattam, hogy számos előnnyel járhat egy olyan tesztelési keretrendszer, ahol a futási idejű programokat metaprogramok segítségével teszteljük. A metaprogramozás terjedését jelentősen hátráltatja a szokásos programfejlesztői eszközök hiánya. Részt vettem egy grafikus metaprogram debugger és olyan vizualizációs eszköz kidolgozásában, amely bemutatja a példányosítás folyamatát kép file-okba kiexportált gráfok formátumában. Ezek az eszközök nagymértékben elősegítik a programozói hibák kijavítását és elkerülését [8, 74]. Különböző objektum-orientált nyelvek eltérően támogatják a paradigmát, minden nyelv kicsit eltérő konstrukciót ad például a metódusok deklarációjának finomhangolásához. Több olyan konstrukciót implementáltunk C++ban, ami a nyelvben eredetileg nincs benne: Java-ban használt felüldefiniálhatatlan final metódusok [101], elrejthetetlen metódusok, amelyek megakadályozzák, hogy eltérő deklarációval (véletlenül) elrejtsünk egy metódust [98], Eiffel-ben létező metódus átnevezés konstrukció [102]. Az Eiffel-ben használt tagok szelektív hozzáférését is megvalósítottuk template metaprogramok segítségével [56]. Ezen konstrukciók sablonok segítésével működnek és használatuk segítségével programozói hibákat lehet elkerülni. A C nyelv szabvány könyvtárának printf függvénye úgy működik, hogy első paramétere egy formázó string ami meghatározza, hogy a további pa-

Alapok 13 ramétereket hogyan kell kiírni az kimenetre. A formázó string értékét a fordítóprogramok nem kezelik, így használata hibákat okozhat. Kidolgoztunk egy metastring könyvtárat, melyben a string-ek értéke fordítás idejű információ. Ezekkel a metastring-ekkel a megírtuk a printf olyan verzióját, amely képes típus ellenőrzéseket elvégezni, így képes fordítás közben kiszűrni a programozói hibákat [104]. Hasonló problémák jöhetnek elő a multicore programozást támogató C++ könyvtár, a FastFlow kapcsán is [2]. A különböző task-ok egy void* (típustalan) pointer segítségével adnak át tetszőleges adatot egymásnak [99]. Ezt a hibalehetőséget sablonok segítségével elimináltuk a könyvtárból, sőt hatékonyabbá is tettük az implementációt: a virtuális függvények alkalmazását fordítási idejű mechanizmusra cseréltük sablonokkal [100]. Részt vettem a C++-hoz tervezett concept konstrukció egy bővítésében is, hogy a private, public és protected módosítók használhatóak legyenek concept map-ekben is [103]. Más nyelvek gyakran másképpen kezelik a generikus elemeket [9]. Különböző modern nyelvekben használatos nyelvi konstrukciót összehasonlítottam [85]. II.2. Generatív és generikus programozás A máig rendkívül széleskörben használt objektum-orientált programozás hiányosságaira fény derült az ezredfordulóra. Kiderültek a gyengeségei, és újabb programozási technikák alakultak ki, amelyekkel ezeket a gyengeségeket próbálták legyőzni. Azok a technikák, amelyek valamilyen eszköz (tool) segítségével generálnak kódot a generatív (generative) módszerek. Sokféle generatív technika létezik: a már említett template metaprogramozás is egy ide tartozó módszer: a fordítóprogram sablonok példányosításán keresztül kódot generál és értékel ki. Az aspektus-orientált programozás a logikailag elkülönülő, de fizikailag egybetartozó kódokat tudja aspektusokba modularizálni, amit egy aspektus-szövő szoftver (weaver) sző össze [48]. Az objektum-orientált jellegzetes gyengeségére az ún. expression problem világít rá [116]. Tekintsük az alábbi nyelvtant [109]: Exp ::= Lit Add Lit ::= (nem-negatív egész) Add ::= Exp + Exp Tegyük fel, hogy megvalósítunk egy print() műveletet, amellyel az output-ra kiírhatjuk a kifejezést. A feladat objektum-orientált megoldásához két különböző megoldás adható. Az első az adatközpontú: minden műveletet

14 Alapok virtuális metódusként definiálunk egy közös bázisosztályban, és minden specifikus osztályban felüldefiniáljuk. Ez a tipikus objektum-orientált megoldás, megvan az a moduláris tulajdonsága, hogy új osztályokat vehetünk fel úgy, hogy a meglévő kódhoz nem kell hozzányúlnunk, pl. kivonáshoz: Exp ::=... Neg Neg ::= - Exp Ha viszont egy új műveletet szeretnénk bevezetni, pl. eval, ami kiértékeli a (rész)kifejezést, akkor minden egyes osztályt módosítanunk kell, hogy specifikusan implementálhassuk a műveletet. A visitor design pattern [29] segítségével a műveleteket lehet osztályként ábrázolni: ilyenkor könnyű újabb műveleteket bevezetni a rendszerbe, de bonyolult feladat új adatszerkezeteket felvenni. Az objektum-orientált paradigma nem támogatja, hogy párhuzamosan az adat- és művelet-centrikusan is kiterjesszük az implementációt. Egy könyvtár esetében viszont, ha új műveleteket (algoritmusokat) kell bevezetni a könyvtár implementációját kellene módosítani, ami nem mindig oldható meg. Az egyik legfontosabb generatív paradigma, a generikus programozás [87] erre a problémára ad választ: az adatszerkezetek és műveletek absztrakt megfogalmazásával a komponensek konfigurálhatóságát és együttműködését megszervezi, miközben elvonatkoztat az érdektelen részletektől [59]. Dolgozatom központi témája a generikus programozáshoz tartozó STL helyes használata, de ezenkívül foglalkoztam aspektus-orientált programok helyességével [81] és metaprogramok helyességével is [73]. II.3. A C++ Standard Template Library A C++ Standard Template Library (STL) egy generikus programozási paradigmán (generic programming paradigm) alapuló könyvtár, mely része a C++ szabvány könyvtárának. Az STL kihasználja a C++ sablonok lehetőségeit, így egy bővíthető, hatékony, mégis flexibilis rendszert alkot. Az STL (és a generikus programozás) központi elve az általánosítás [53]. Scott Meyers az STL-t a szabványkönyvtár legforradalmibb részének tartja [54]. Kiemeli, hogy a felépítése, a rugalmassága, bővíthetősége, a szabvány miatti hatékonysága teszi nagyon jól használhatóvá. Szerinte az STL nem szoftver, hanem konvenciók halmaza, és emiatt forradalmi. Dewhurst viszont azt a tulajdonságát emeli ki, hogy a tárolók szerkezetükkel és működésükkel kapcsolatos döntések már fordítási időben megszületnek [17]. Emiatt hatékony és kicsi kód készül, mely teljesítmény tekintetében pontosan alkalmazkodik az adott felhasználási módhoz. Bruce Eckel az STL-nek azt a jó

Alapok 15 tulajdonságát is kiemeli, hogy teljesen platformfüggetlen [23]. Karbantartásilag az STL egyik legnagyobb előnye a szabványos névhasználatban rejlik: az STL komponensei minden C++ programozó számára ugyanazt jelentik. Ez olyan szemantikai többlettel jár, amit semmilyen kézzel írt kód nem tud megadni. A fenti jó tulajdonságok elsősorban az STL egyedi szerkezetével magyarázhatóak. Az STL alapvető komponensei: konténerek (containers), algoritmusok (algorithms), iterátorok (iterators), funktorok (functors), átalakítók (adaptors), allokátorok (allocators). A konténerek (pl. vector, set, map, stb.) alapvető feladata az adatok memóriában történő elhelyezése, tárolása és a memória konzisztensen tartása. A konténerek csoportosíthatóak: szekvenciális és asszociatív konténerekre. (Meyers egy másik csoportosítást is használ az STL konténerek kapcsán: láncolt és egybefüggő-memória konténerekre [53].) Az STL-ben három szabványos szekvenciális konténer sablon található: list, vector, deque, valamint a string konténer, ami konkrétan karakterek tárolására optimalizált. Ezeknél a felhasználó definiálja, hogy az elemek hova kerüljenek a konténerben. A vector egy olyan konténer, amely garantáltan egybefüggő tárterületen tárolja az elemeket, így gyorsan elérhető tetszőleges eleme, de a törlés és beszúrás műveletek csak a konténer végén hatékonyak. A vector közvetlen elérésű iterátorokat biztosít, amelynek segítségével az STL összes algoritmusa használható. A list konténer egy kétirányú láncolt lista, amelybe tetszőleges helyre hatékonyan lehet beszúrni illetve tetszőleges helyről lehet hatékonyan törölni, de a konténer tetszőleges eleme csak lineáris időben érhető el. A list konténer bidirectional, azaz kétirányú iterátorokat garantál. A deque egy kettős végű sor, ahol a konténer két végén lehet hatékonyan megváltoztatni a konténer méretét. Szintén random access kategóriájú iterátorokat biztosít. Ugyan ezek a konténerek hasonló interface-szel rendelkeznek, nem célszerű őket egymással felcserélhetőnek feltételezni. A vector és a string két nagyon hasonló adatszerkezet. Az interface különbségein túl a legfontosabb szemantikai különbség a kettő között a másolásban rejlik: a vector copy konstruktora és értékadó operátora kötelezően a sablon paraméter értékadó operátorával másolja a konténer összes elemét, a string viszont használhat referenciaszámlálást. Az STL-ben négy szabványos asszociatív sablon található: set, multiset, map, multimap. Az asszociatív konténerek rendezetten tárolják a bennük lévő elemeket, de a mögöttes valódi adatszerkezetet nem definiálja a C++ szabványa. Jellemzően piros-fekete fákat használnak az STL implementációk. A map és a multimap kulcs-érték párokat tartalmaz, a set és a multiset csak kulcsokat. A set-ben, map-ben nem lehet több ekvivalens

16 Alapok kulcs, egyedieknek kell lenniük. Ezzel szemben a multiset és a multimap támogatja az ekvivalens kulcsok multiplicitását. Az iterátorok garantálják az algoritmusok és a konténerek függetlenségét. Egy egységes interface segítségével definiálják a memóriában elhelyezett elemek elérését. Ez az interface a pointer-aritmetikán alapul: mutatók (pointerek) segítségével tömbökön végig lehet iterálni. Az iterátorok a pointerek absztrakciójának tekinthetőek. A pointerek használhatóak iterátorként is, a tömbökön az STL algoritmusai meghívhatóak. A pointer-aritmetika alapvető műveletei: prefix és postfix operator++: a következő elemre lépteti a pointert. operator*: a pointer által mutatott elem lekérdezése operator==: a két pointer ugyanarra az elemre hivatkozik-e prefix és postfix operator--: a megelőző elemre lépteti a pointert, A pointerek segítségével a tömbben tetszőleges pozícióra lehet ugrani bárhonnan. Ez nem igaz az összes STL-ben definiált iterátorra. Az STL iterátorai különböző kategóriákba esnek a képességeik alapján [46]. A kategóriák egy hierarchiát alakítottak ki a különböző iterátorok között: Ezek a kategóriák nyelvi szemszögből nincsenek megkülönböztetve, a C++ sablonjai megszorítás nélküliek, a C++ nyelv jelenleg nem tartalmaz concepteket. Azok az iterátorok teljesítik az input iterátorok elvárásait, amelyekkel egyszer végig lehet menni egy intervallumon és a benne lévő elemeket el lehet egyszer érni olvasásra. Ilyen iterátorokat használ például a for each algoritmus. Azok az iterátorok teljesítik az output iterátorok elvárásait, amelyekkel az elemek szekvenciálisan elérhetőek és írhatóak. Ilyet vár például a copy

Alapok 17 harmadik paramétereként. Az ostream iterator<t> sablon példányai tipikus output kategóriájú iterátorok. Azok az iterátorok teljesítik a forward iterátorok elvárásait, amelyekkel szekvenciálisan végig lehet menni az intervallumon, és az iterátorok olvasni és írni is tudják az elemeket. Mivel az input iterátorokkal szemben a forward iterátorokról másolatok készülhetnek az algoritmusok törzsében, azaz többször is hivatkozhatnak már elért elemet, akár többször is bejárható egy intervallum. Mivel a max element algoritmus iterátort ad vissza az input intervallum legnagyobb értékére, forward kategóriájú iterátort vár. Ha csak az intervallum legnagyobb értéket adná vissza, akkor input iterátort várna. Azok a forward iterátorok teljesítik a kétirányú (bidirectional) iterátorok elvárásait, amelyek képesek nem csak a következő, hanem az előző elemet is elérni. Például a list konténer iterátorai bidirectional iterátorok. Azok az iterátorok teljesítik a közvetlen vagy véletlen elérésű (randomaccess) iterátorok elvárásait, amelyek bidirectional iterátorok és képesek gyorsan (konstans időben) egynél több elemmel is növelni vagy csökkenteni a relatív címzés megvalósításához. Ezenkívül támogatniuk kell iterátorok öszszehasonlítását operator<, operator>=, stb. műveletekel. Ilyen kategóriájú iterátort vár például a sort algoritmus és például a vector konténer iterátorai is random-access kategóriájúak. A konténerek belső típusként négy fajta iterátor típust garantálnak: iterator const iterator reverse iterator const reverse iterator A konténerek begin() és end() tagfüggvényeivel tudunk konténer iterátorokat létrehozni. A begin() létrehoz egy olyan iterátort, ami konténer első elemére mutat, az end() pedig egy olyat, ami a konténer extremális végére mutat. A reverse iterátorok létrehozásához a konténerek rbegin() és rend() tagfüggvényei használhatóak. A const iterator és a const reverse iterator nem engedi az iterátor objektumokon keresztül megváltoztatni a konténer hivatkozott értékét, azt csak olvasni tudja. A C++ konstans-biztonságának fontos összetevői ezek az iterátorok. A reverse iterátorok (reverse iterator és const reverse iterator) az iterátorokhoz képest fordított irányban haladnak, a konténer végétől indulnak és a konténer elejéig mennek. Kényelmesen használhatóak, amikor például egy elem utolsó előfordulását kell megkeresni a find algoritmussal.

18 Alapok A konténerek iterátorain kívül az STL még biztosít néhány iterátor sablont, amelyekkel a stream-eken (például file-okon, standard input-on vagy output-on) lehet iterálni: istream iterator<t> ostream iterator<t> istreambuf iterator<t> ostreambuf iterator<t> Az istream iterator<t> és az ostream iterator<t> iterátorokat formázott input/output-hoz tervezték, ezek a sablon paraméter típusának kiíró (operator<<) és beolvasó (operator>>) operátorát hívják meg. Ezzel szemben, az istreambuf iterator<t> és ostreambuf iterator<t> iterátorokat karakterenkénti input/output-hoz tervezték, kevésbé rugalmasak, de a karakteres input/output-ot gyorsabban dolgozzák fel [53]. Követve az eddigi analógiát, azt mondhatnánk, hogy a függvényeket algoritmusokká általánosították, melyek a használt iterátorok típusa alapján paraméterezhetőek [53]. Az algoritmusok konténer-független függvénysablonok gyakori feladatokra, mint például keresés (pl. find, find if), stb.), rendezés (pl. partial sort, sort), másolás (pl. copy, unique copy), számlálás (pl. count, count if). Az algoritmusok belül valamilyen ciklusra képződnek le, azaz algoritmusok olyan függvények absztrakciói, amelyekben ciklusok vannak [4]. Az STL-nek 60 szabványos algoritmusa van [92]. Az algoritmusok konténer-függetlenek, de nem igaz az, hogy az összes algoritmus az összes konténerrel együttműködik, például a sort algoritmus random-access iterátorokat vár, így ha csak kétirányú list iterátorokat adunk át, akkor fordítási hibát kapunk. Az STL egyik nem elhanyagolható tulajdonsága, hogy az egyes tagfüggvények és algoritmusok futási ideje az intervallum vagy konténer méretéhez viszonyítva aszimptotikusan rögzített, amit minden implementációnak be kell tartania. Így például garantált, hogy a count algoritmus lineáris futási idejű, a set<key>::count tagfüggvény pedig logaritmikus. Az STL egyik fontos komponense a funktor [5]. Funktorok segítségével felhasználói kódrészleteket lehet hatékonyan végrehajtani a könyvtáron belül: funktorok definiálhatnak rendezéseket, predikátumokat, vagy valamilyen műveletet, amit végre szeretnénk hajtani az elemeken. Technikailag a funktorok egyszerű osztályok, amelyek rendelkeznek egy operator()-ral. (A funktorok ügyét nagymértékben segíti, hogy a paraméterek száma, a többi operátorral szemben, nincs előre definiálva az operator()

Alapok 19 esetében.) Jellemzően két tipikus helyen haszálnak funktorokat az STL-ben: algoritmusok paramétereként és asszociatív konténerek rendezéséhez. Algoritmusok esetében a funktorokhoz bevezetnek egy extra sablon paramétert, és az algoritmus egy ilyen típusú objektumot vár. Belül az algoritmus kódjában hivatkozik a paraméter operator()-ra: ez lehet egy függvénypointer dereferálása vagy a funktor operator()-a. Mivel ekkor a fordítóprogram paraméterdedukcióval levezeti a funktor típusát, képes inline-osítani a felhasználói kódrészletet. A függvénypointer esetében nem optimalizálhat a fordítóprogram, mert futás idejű információként kezeli azt, hogy a pointer hova (melyik függvényre) mutat. A funktorok osztályok, így lehetnek adattagjai, amelyek a külön hivatkozások között információ áramlást biztosíthatnak, és lehetnek konstruktorai, amelyeken keresztül extra paraméterek adhatóak át. Az aszszociatív konténerek esetében, magának a konténernek van egy extra sablon paramétere, ami a rendezés funktor típusát definiálja. Az alábbi példa bemutatja a for each algoritmus implementációját, amely gyakran használ funktort: template <class InputIterator, class UnaryFunction> UnaryFunction for_each( InputIterator first, InputIterator last, UnaryFunction f ) while( first!= last ) f( *first++ ); return f; A funktorokat lehet osztályozni, megkülönböztetünk unáris és bináris funktorokat. Az unáris funktoroknak operator()-ának egy, a bináris funktoroknak operator()-ának két paramétere van. Emellett megkülönböztetjük a predikátumokat, ezek olyan funktorok, amelyek operator()-a visszatérési érték típusa bool vagy konvertálódik bool-lá. Adaptálható (alkalmazkodóképes) funktorok a funktorok azon részcsoportja, amelyre a funktor adaptorok alkalmazhatóak. Kétféle szabványos funktor adaptor található az STL-ben: binder-ek és negálók [45]. A binderek (bind1st és bind2nd) egy bináris funktorból unárisat készítenek a funktor egyik paraméterének rögzítésével. A negáló adaptor-ok (not1 és not2) egy bináris vagy unáris predikátumot negálnak. Ahhoz, hogy egy funktor adaptálható legyen biztosítania kell néhány typedef-et, ami alapján

20 Alapok az adaptor definiálja annak az adaptált funktor operator()-ának viszszatérési érték és paraméter(ek) típusát. A következő typedef-ekre van szükség egy unáris funktor adaptálásához: argument type, result type. Egy bináris funktor adaptálásához szükség van a first argument type, a second argument type és result type szinonímákra. Ezeket a legegyszerűbben úgy lehet beállítani, ha a funktorunk a megfelelően példányosított unary function vagy binary function sablonból származik [17]. Példaképpen nézzük meg a következő kódrészletet: struct IsEven: std::unary_function<int, bool> bool operator()( const int& i ) const return 0 == i % 2; ; //... std::vector<int> v; //... std::vector<int>::iterator i = std::find_if( v.begin(), v.end(), std::not1( IsEven() ) ); Az IsEven egy adaptálható funktor típus, amely eldönti egy int értékről, hogy páros-e. A std::not1 az unáris funktort negálja, így a find if az első páratlan számot keresi a vector-ban. A szabványos könyvtárban már előre adott néhány funktor sablon, pl. a relációs műveleteken alapuló bináris funktorok: less, greater, stb., illetve néhány aritmetikai bináris műveleten alapuló sablon: például összeadás funktor sablonja a plus, a szorzásé a multiplies, stb.. Az STL átalakítói nem önálló komponensei a könyvtárnak, valamely komponenst alakítanak át egy eltérő funkcionalitás érdekében: léteznek konténer adaptorok, (tag)függvény adaptorok, funktor adaptorok, iterátor adaptorok. Az STL-ben három konténer adaptor létezik: queue, priority queue és stack. Ezek valamely paraméterezhető szekvenciális konténert alakítják át oly módon, hogy szűkített lehetőséggel bírjon specifikus adatszerkezetként. Az algoritmusok, amikor felhasználói kódrészletet hívnak egy globális függvényt hívnak meg: ez vagy egy globális függvényre mutató pointer vagy

Alapok 21 egy funktor típus operator()-a, de semmiképpen nem egy objektum tagfüggvényének a meghívása és nem egy pointeren keresztüli tagfüggvény meghívása. Ez utóbbi esetekben van szükségünk a tagfüggvény átalakítókra: mem fun és mem fun ref. Mindkettő kap egy tagfüggvény pointert, amiből a szükséges paramétereket levezeti és létrehoz egy olyan funktort, amelyben egy megfelelő típusú tagfüggvény pointer szerepel adattagként. Ennek az implementációs funktornak az operator()-a delegálja a hívást a tagfüggvény pointerhez. A mem fun ref felelős az objektumokon keresztüli tagfüggvény hívásért, a mem fun a pointereken keresztüli tagfüggvény hívásért. A ptr fun viselkedése is hasonló: egy globális függvényből készít o- lyan funktort, amely egy függvénypointeren keresztül hívja meg a felhasználói kódrészletet. A függvénnyel szemben a funktor alkalmazkodóképes, azaz alkalmazhatjuk a negálókat és binder-öket. Tehát, ha adott például egy predikátumfüggvényünk és negálni szeretnénk, akkor előtte ptr fun-nal funktorrá kell alakítanunk. A funktor adaptorokat a funktoroknál már bemutattam: két predikátum negáló (not1 és not2) és két binder (bind1st és bind2nd) funktor adaptor található a szabványos STL-ben. Az STL iterátor adaptorainak az a céljuk, hogy iterátort szimuláljanak specifikus céllal: konténerhez adjanak új elemeket. A konténerek iterátorai nem érik el azt a konténer objektumot, amelynek elemeire hivatkoznak, nem tudják meghívni a tagfüggvényeit, így nem tudnak elemeket hozzáadni az adatszerkezethez: csak meglévő elemeket érik el írási vagy olvasási céllal. Ez másolási algoritmusoknál problémás lehet, ha több elemet másolunk, mint amennyi átírható elem szerepel az output-ban. Az STL iterátor adaptorai megkapják a konténert (típussal együtt), így meghívhatóak azok a metódusai, amelyekkel új elemek vehetőek hozzá. A különféle beszúrásokhoz különböző adaptorok használhatóak: a back insert iterator a konténerek push back metódusát hívja meg, a front insert iterator pedig a push front metódusát. Mivel például az asszociatív konténereknek ilyen tagfüggvényei nincsenek, ekkor a insert iterator képes az insert tagfüggvény hívását kikényszeríteni az algoritmuson keresztül. A későbbiekben használni fogjuk az iterator traits sablont. Anélkül írunk le bizonyos iterátorhoz kapcsolódó típusokat, hogy magát az iterátorok kódját megváltoztatnánk, ezért ez egy nem-intrúzív technika. Ennek a sablonnak a specializációi különféle typedef-eket tartalmaznak, amelyekre szükség lehet az algoritmusok implementálásakor, például, hogy milyen típusú objektumokra hivatkoznak (value type). Az általános implementációja (ezt lehet specializálni) a következőképpen néz ki:

22 Alapok template <class T> struct iterator_traits typedef typename T::iterator_category iterator_category; typedef typename T::value_type value_type; typedef typename T::difference_type difference_type; typedef typename T::pointer pointer; typedef typename T::reference reference; ; A specializációk létrehozásához az iterator sablon bázisosztályt kell példányosítani. Az iterator traits sablon segítségével az algoritmusok túlterhelhetők az iterátor kategóriája alapján a tag dispatch-nek [96] nevezett technika segítségével. Ennek mintapéldája az advance algoritmus, amely a paraméterül kapott iterátort lépteti előre a paraméterül kapott értékkel. A túlterhelés miatt véletlen-elérésű iterátorok esetén a futás ideje konstans, egyébként lineáris. A túlterhelést úgy valósítják meg, hogy a szabványos deklarációra illeszkedő implementáció továbbhív eggyel több paraméterrel. Az extra paraméter egy default konstruált objektum, amelynek típusa az iterátor kategóriáját reprezentáló dummy típus. Ha ez std::random access iterator tag típusú, akkor közvetlen elérésű iterátort kapott az algoritmus és ezt kihasználhatja az implementáció: template <class InputIterator, class Distance> void advance( InputIterator& i, Distance n ) advance( i, n, typename std::iterator_traits<inputiterator>::iterator_category() ); template <class InputIterator, class Distance> void advance( InputIterator& i, Distance n, std::random_access_iterator_tag ) i += n; template <class InputIterator, class Distance> void advance( InputIterator& i,

Alapok 23 Distance n, std::bidirectional_iterator_tag ) for( Distance j = 0; j < n; ++j ) ++i; Az allokátorokat eredetileg a memóriamodellek absztrakciójaként fejlesztették ki, hogy ne kelljen megkülönböztetni a near és a far pointereket bizonyos 16-bites operációs rendszerekben. Arra is tervezték az allokátorokat, hogy elősegítse a memóriakezelők fejlesztését. A memóriaallokálás testreszabásához az összes szabványos STL konténer ad megoldást: az utolsó sablon paraméter az allokátor típusát definiálja. Van default értéke, de másik típus is megadható helyette. Az operator new-hoz és az operator new[]-hoz hasonlóan az STL allokátorok felelősek a nyers memória allokációjáért (és deallokációjáért), de az allokátorok kliensei kevés hasonlóságot hordoznak az operator new-hoz, az operator new[]-hoz vagy akár a malloc-hoz viszonyítva. Végül (de talán a legjelentősebb), hogy a szabványos konténerek nagyrésze sosem kér memóriát az allokátorától. Ennek az az oka, hogy láncolt adatszerkezetek (pl. list vagy az asszociatív adatszerkezetek nem a konténer value type typedef-je alapján kell memóriát allokálniuk, hanem egy belső implementációs struktúra elemeinek (pl. List node) [53]. Saját allokátorokat olyan helyzetben érdemes írni, ha a default allokátor szál-biztos (thread-safe) és nincs erre szükség, vagy speciális heap memóriát szeretnénk használni, ahol a konténer elemei egymáshoz közel helyezkednek el. Több folyamat által használt osztott memória használatakor is érdemes lehet saját allokátort írni. Az új C++ szabvány (C++11) magát az STL-t is bővítette. Ezek a bővítmények nem oldják meg a dolgozatban szereplő problémákat. A C++11 által biztosított STL új lehetőségeit később ismertetem az A függelékben. Az STL-t szekvenciális környezetre tervezték, ezért használata a szűk keresztmetszete lehet a multicore (többmagos) fejlesztéseknek. Részt vettem egy multicore környezetre optimalizált STL fejlesztésében is [105, 106, 107, 108]. Eközben végtelen intervallumok iterátorainak támogatását is megvalósítottuk [51].

24 Alapok II.4. Motivációs példák Ebben a fejezetben bemutatom azokat a nehézségeket, amelyekkel a programozóknak szembe kell nézniük az STL használatakor. Ismertetem azokat a problémákat, amelyeket az STL hibás használata okozhat. Ezek a hibák okozhatnak nehezen értelmezhető fordítási hibaüzeneteket, nem portábilis kódot, hibás futási eredményeket, memória szivárgást, korrupttá vagy inkonzisztenssé váló adatszerkezeteket illetve szükségtelen hatékonyságromlást. A most felsorolt problémák egy részére dolgozatom megoldást kínál, más problémák további kutatások tárgyai. II.4.1. Fordítási hibaüzenetek Az egyik leggyakoribb kritika ami az STL-t éri, az a fordítási hibaüzenetek érthetetlensége. A hosszú fordítási hibaüzenetek az STL implementációjára hivakoznak és nehéz kideríteni a probléma valódi okát. Ennek gyakori oka, hogy a C++ sablonok esetében nincs nyelvi eszköz a sablon paraméterekkel kapcsolatos elvárások leírására. Vegyük például az alábbi kódrészletet: std::list<int> a; std::sort( a.begin(), a.end() ); A kód látszólag rendben van, a sort (konténer-független) algoritmussal rendezni próbálunk egy lista adatszerkezetet. Mégis az alábbi hibaüzenetet kapjuk a fordítóprogramtól: /usr/include/c++/4.3/bits/stl_algo.h: In function void std::sort(_raiter, _RAIter) [with _RAIter = std::_list_iterator<int>] : listsort.cpp:7: instantiated from here /usr/include/c++/4.3/bits/stl_algo.h:4783: error: no match for operator- in last - first /usr/include/c++/4.3/bits/stl_algo.h: In function void std:: final_insertion_sort(_randomaccessiterator, _RandomAccessIterator) [with _RandomAccessIterator = std::_list_iterator<int>] : /usr/include/c++/4.3/bits/stl_algo.h:4785: instantiated from void std::sort(_raiter, _RAIter) [with _RAIter = std::_list_iterator<int>] listsort.cpp:7: instantiated from here

Alapok 25 /usr/include/c++/4.3/bits/stl_algo.h:1827: error: no match for operator- in last - first /usr/include/c++/4.3/bits/stl_algo.h:1829: error: no match for operator+ in first + 16 /usr/include/c++/4.3/bits/stl_algo.h:1830: error: no match for operator+ in first + 16 /usr/include/c++/4.3/bits/stl_algo.h: In function void std:: insertion_sort(_randomaccessiterator, _RandomAccessIterator) [with _RandomAccessIterator = std::_list_iterator<int>] : /usr/include/c++/4.3/bits/stl_algo.h:1833: instantiated from void std:: final_insertion_sort(_randomaccessiterator, _RandomAccessIterator) [with _RandomAccessIterator = std::_list_iterator<int>] /usr/include/c++/4.3/bits/stl_algo.h:4785: instantiated from void std::sort(_raiter, _RAIter) [with _RAIter = std::_list_iterator<int>] listsort.cpp:7: instantiated from here /usr/include/c++/4.3/bits/stl_algo.h:1753: error: no match for operator+ in first + 1 /usr/include/c++/4.3/bits/stl_algo.h:1833: instantiated from void std:: final_insertion_sort(_randomaccessiterator, _RandomAccessIterator) [with _RandomAccessIterator = std::_list_iterator<int>] /usr/include/c++/4.3/bits/stl_algo.h:4785: instantiated from void std::sort(_raiter, _RAIter) [with _RAIter = std::_list_iterator<int>] listsort.cpp:7: instantiated from here /usr/include/c++/4.3/bits/stl_algo.h:1759: error: no match for operator+ in i + 1 A hiba valódi oka az, hogy a sort algoritmus random-access kategóriájú iterátorokat vár, de a list konténernek csak bidirectional iterátorai vannak. A sort implementációjában azok a műveletek, amelyek kihasználják a közvetlen elérést fordítási hibákat okoznak, hiszen ilyen műveleteket a list bejárói nem támogatnak. Sajnos a hibaüzenet nem fejezi ki elég világosan, hogy a sort algoritmus nem használható a list konténerrel. Az ilyen jellegű problémákra olyan metaprogram könyvtárak adnak megoldást, amelyek fordítási időben már korábban ellenőrzik, hogy a sablon paraméter megfelele az elvárásoknak [118]. Ezek a könyvtárak azonban közel sem teljesek és implementáció-függőek. A kutatók a mai napig dolgoznak a concept-eken,

26 Alapok melyek nyelvi szintű konstrukcióként támogatják a sablonok típus paramétereinek ellenőrzését [103]. Ugyanennek a jelenségnek egy másik oka is van: a fordítóprogramok a hibaüzenetekben nem mindig arra az azonosítóra (vagy nem ugyanabban a formátumban) hivatkoznak, mint ami a forráskódban szerepel. A string-ek kezeléséhez az std::string típus használják a programozók. Maga az std::string nem önálló típus, hanem egy typedef. Az std::string egy szinonímája a std::basic string<char, char traits<char>, allocator<char> > típusnak. A hibaüzenetekben viszont ez utóbbi típusra hivatkozik a fordítóprogram, akkor is, ha a programozó az std::string-ként használja. A fenti hibaüzenetben ilyen a List iterator<int> implementáció-specifikus azonosító is, ami az std::list<int>::iterator szabványos típus álneve. Az asszociatív konténerek alatt lévő adatszerkezetet nem definiálja a C++ szabványa. A leggyakrabban piros-fekete fák [12] segítségével implementálják az asszociatív adatszerkezeteket. Egy implementációtól függő segéd sablon típusban (pl. Rb tree vagy Tree) megvalósítják az adatszerkezetet, a szabványos konténerek pedig ezeket a segédtípusokat használják. Vegyük most az alábbi hibás kódrészletet: std::set<std::string> a; a.insert( a ); A g++ fordítóprogram az alábbi hibaüzenetet adja az előző kódrészletre: seterr.cpp:11: error: no matching function for call to std::set<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::insert( std::set<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&) /usr/include/c++/4.3/bits/stl_set.h:378: note: candidates are: std::pair<typename std::_rb_tree<_key, _Key, std::_identity<_key>, _Compare, typename _Alloc::rebind<_Key>::other>::const_iterator, bool> std::set<_key, _Compare, _Alloc>::insert(const _Key&) [with _Key = std::basic_string<char, std::char_traits<char>, std::allocator<char> >, _Compare = std::less<std::basic_string<char,

Alapok 27 std::char_traits<char>, std::allocator<char> > >, _Alloc = std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >] /usr/include/c++/4.3/bits/stl_set.h:405: note: typename std::_rb_tree<_key, _Key, std::_identity<_key>, _Compare, typename _Alloc::rebind<_Key>::other>::const_iterator std::set<_key, _Compare, _Alloc>::insert(typename std::_rb_tree<_key, _Key, std::_identity<_key>, _Compare, typename _Alloc::rebind<_Key>::other>::const_iterator, const _Key&) [with _Key = std::basic_string<char, std::char_traits<char>, std::allocator<char> >, _Compare = std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, _Alloc = std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >] Látható, hogy a hibaüzenetben olyan típusok is megjelennek nagy menynyiségben, melyek a forráskódban sehol sem láthatóak: például konténer default sablonparaméterei és egyéb implementációs segédtípusok. Világos, hogy ezek a hibaüzenetek megértése nagy gyakorlatot kíván. Létezik platform specifikus eszköz a hibaüzenetek megértéséhez [117], de nincsen általánosan használható kényelmes eszköz erre a problémára. II.4.2. Invalid iterátorok Az iterátorok központi elemei az STL-nek: összekötik az algoritmusokat a konténerekkel, és a konténerek bejárását biztosítják. Sajnos azonban az iterátor objektumok élettartama nem feltétlenül esik egybe a hivatkozott objektum élettartalmával. Előfordulhat, hogy egy iterátor olyan objektumra hivatkozik, ami már nincs a memóriában vagy máshova került. A vector konténer sablon tipikus implementációja olyan, hogy lefoglal egy egybefüggő tárterületet valamekkora kapacitással. Ha betelik ez a kapacitás, akkor lefoglal egy nagyobb (jellemzően kétszer akkora) egybefüggő tárterületet, az elemeket átmásolja a régi tárterületről az újra, és a régi tárterületet felszabadítja a konténer. Nem garantált, hogy ezt a konténerhez tartozó iterátorok észreveszik. Több STL implementációnál az iterátorok továbbra is régi tárterületre hivatkoznak, és ha hivatkozunk ezekre az iterátorokra, akkor az nemdefiniált eredményhez vezet. Vegyük például az alábbi kódrészletet: std::vector<int> v; int x; //...