try { // rabotaem s f } catch (...) { fclose(f); throw; } fclose(f); } Vsya chast' funkcii, rabotayushchaya s fajlom f, pomeshchena v proveryaemyj blok, v kotorom perehvatyvayutsya vse osobye situacii, zakryvaetsya fajl i osobaya situaciya zapuskaetsya povtorno. Nedostatok etogo resheniya v ego mnogoslovnosti, gromozdkosti i potencial'noj rastochitel'nosti. K tomu zhe vsyakoe mnogoslovnoe i gromozdkoe reshenie chrevato oshibkami, hotya by v silu ustalosti programmista. K schast'yu, est' bolee priemlemoe reshenie. V obshchem vide problemu mozhno sformulirovat' tak: void acquire() { // zapros resursa 1 // ... // zapros resursa n // ispol'zovanie resursov // osvobozhdenie resursa n // ... // osvobozhdenie resursa 1 } Kak pravilo byvaet vazhno, chtoby resursy osvobozhdalis' v obratnom po sravneniyu s zaprosami poryadke. |to ochen' sil'no napominaet poryadok raboty s lokal'nymi ob容ktami, sozdavaemymi konstruktorami i unichtozhaemymi destruktorami. Poetomu my mozhem reshit' problemu zaprosa i osvobozhdeniya resursov, esli budem ispol'zovat' podhodyashchie ob容kty klassov s konstruktorami i destruktorami. Naprimer, mozhno opredelit' klass FilePtr, kotoryj vystupaet kak tip FILE* : class FilePtr { FILE* p; public: FilePtr(const char* n, const char* a) { p = fopen(n,a); } FilePtr(FILE* pp) { p = pp; } ~FilePtr() { fclose(p); } operator FILE*() { return p; } }; Postroit' ob容kt FilePtr mozhno libo, imeya ob容kt tipa FILE*, libo, poluchiv nuzhnye dlya fopen() parametry. V lyubom sluchae etot ob容kt budet unichtozhen pri vyhode iz ego oblasti vidimosti, i ego destruktor zakroet fajl. Teper' nash primer szhimaetsya do takoj funkcii: void use_file(const char* fn) { FilePtr f(fn,"w"); // rabotaem s f } Destruktor budet vyzyvat'sya nezavisimo ot togo, zakonchilas' li funkciya normal'no, ili proizoshel zapusk osoboj situacii. 9.4.1 Konstruktory i destruktory Opisannyj sposob upravleniya resursami obychno nazyvayut "zapros resursov putem inicializacii". |to universal'nyj priem, rasschitannyj na svojstva konstruktorov i destruktorov i ih vzaimodejstvie s mehanizmom osobyh situacij. Ob容kt ne schitaetsya postroennym, poka ne zavershil vypolnenie ego konstruktor. Tol'ko posle etogo vozmozhna raskrutka steka, soprovozhdayushchaya vyzov destruktora ob容kta. Ob容kt, sostoyashchij iz vlozhennyh ob容ktov, postroen v toj stepeni, v kakoj postroeny vlozhennye ob容kty. Horosho napisannyj konstruktor dolzhen garantirovat', chto ob容kt postroen polnost'yu i pravil'no. Esli emu ne udaetsya sdelat' eto, on dolzhen, naskol'ko eto vozmozhno, vosstanovit' sostoyanie sistemy, kotoroe bylo do nachala postroeniya. Dlya prostyh konstruktorov bylo by ideal'no vsegda udovletvoryat' hotya by odnomu usloviyu - pravil'nosti ili zakonchennosti ob容ktov, i nikogda ne ostavlyat' ob容kt v "napolovinu postroennom" sostoyanii. |togo mozhno dobit'sya, esli primenyat' pri postroenii chlenov sposob "zaprosa resursov putem inicializacii". Rassmotrim klass X, konstruktoru kotorogo trebuetsya dva resursa: fajl x i zamok y (t.e. monopol'nye prava dostupa k chemu-libo). |ti zaprosy mogut byt' otkloneny i privesti k zapusku osoboj situacii. CHtoby ne uslozhnyat' rabotu programmista, mozhno potrebovat', chtoby konstruktor klassa X nikogda ne zavershalsya tem, chto zapros na fajl udovletvoren, a na zamok net. Dlya predstavleniya dvuh vidov resursov my budem ispol'zovat' ob容kty dvuh klassov FilePtr i LockPtr (estestvenno, bylo by dostatochno odnogo klassa, esli x i y resursy odnogo vida). Zapros resursa vyglyadit kak inicializaciya predstavlyayushchego resurs ob容kta: class X { FilePtr aa; LockPtr bb; // ... X(const char* x, const char* y) : aa(x), // zapros `x' bb(y) // zapros `y' { } // ... }; Teper', kak eto bylo dlya sluchaya lokal'nyh ob容ktov, vsyu sluzhebnuyu rabotu, svyazannuyu s resursami, mozhno vozlozhit' na realizaciyu. Pol'zovatel' ne obyazan sledit' za hodom takoj rabotoj. Naprimer, esli posle postroeniya aa i do postroeniya bb vozniknet osobaya situaciya, to budet vyzvan tol'ko destruktor aa, no ne bb. |to oznachaet, chto esli strogo priderzhivat'sya etoj prostoj shemy zaprosa resursov, to vse budet v poryadke. Eshche bolee vazhno to, chto sozdatelyu konstruktora ne nuzhno samomu pisat' obrabotchiki osobyh situacij. Dlya trebovanij vydelit' blok v svobodnoj pamyati harakteren samyj proizvol'nyj poryadok zaprosa resursov. Primery takih zaprosov uzhe neodnokratno vstrechalis' v etoj knige: class X { int* p; // ... public: X(int s) { p = new int[s]; init(); } ~X() { delete[] p; } // ... }; |to tipichnyj primer ispol'zovaniya svobodnoj pamyati, no v sovokupnosti s osobymi situaciyami on mozhet privesti k ee ischerpaniyu pamyati. Dejstvitel'no, esli v init() zapushchena osobaya situaciya, to otvedennaya pamyat' ne budet osvobozhdena. Destruktor ne budet vyzyvat'sya, poskol'ku postroenie ob容kta ne bylo zaversheno. Est' bolee nadezhnyj variant etogo primera: template<class T> class MemPtr { public: T* p; MemPtr(size_t s) { p = new T[s]; } ~MemPtr() { delete[] p; } operator T*() { return p; } } class X { MemPtr<int> cp; // ... public: X(int s):cp(s) { init(); } // ... }; Teper' unichtozhenie massiva, na kotoryj ukazyvaet p, proishodit neyavno v MemPtr. Esli init() zapustit osobuyu situaciyu, otvedennaya pamyat' budet osvobozhdena pri neyavnom vyzove destruktora dlya polnost'yu postroennogo vlozhennogo ob容kta cp. Otmetim takzhe, chto standartnaya strategiya vydeleniya pamyati v S++ garantiruet, chto esli funkcii operator new() ne udalos' vydelit' pamyat' dlya ob容kta, to konstruktor dlya nego nikogda ne budet vyzyvat'sya. |to oznachaet, chto pol'zovatelyu ne nado opasat'sya, chto konstruktor ili destruktor mozhet byt' vyzvan dlya nesushchestvuyushchego ob容kta. Teoreticheski dopolnitel'nye rashody, trebuyushchiesya dlya obrabotki osobyh situacij, kogda na samom dele ni odna iz nih ne voznikla, mogut byt' svedeny k nulyu. Odnako, vryad li eto verno dlya rannih realizaciyah yazyka. Poetomu budet razumno v kritichnyh vnutrennih ciklah programmy poka ne ispol'zovat' lokal'nye peremennye klassov s destruktorami. 9.4.2 Predosterezheniya Ne vse programmy dolzhny byt' ustojchivy ko vsem vidam oshibok. Ne vse resursy yavlyayutsya nastol'ko kritichnymi, chtoby opravdat' popytki zashchitit' ih s pomoshch'yu opisannogo sposoba "zaprosa resursov putem inicializacii". Est' mnozhestvo programm, kotorye prosto chitayut vhodnye dannye i vypolnyayutsya do konca. Dlya nih samoj podhodyashchej reakciej na dinamicheskuyu oshibku budet prosto prekrashchenie scheta (posle vydachi sootvetstvuyushchego soobshcheniya). Osvobozhdenie vseh zatrebovannyh resursov vozlagaetsya na sistemu, a pol'zovatel' dolzhen proizvesti povtornyj zapusk programmy s bolee podhodyashchimi vhodnymi dannymi. Nasha shema prednaznachena dlya zadach, v kotoryh takaya primitivnaya reakciya na dinamicheskuyu oshibku nepriemlema. Naprimer, razrabotchik biblioteki obychno ne v prave delat' dopushcheniya o tom, naskol'ko ustojchiva k oshibkam, dolzhna byt' programma, rabotayushchaya s bibliotekoj. Poetomu on dolzhen uchityvat' vse dinamicheskie oshibki i osvobozhdat' vse resursy do vozvrata iz bibliotechnoj funkcii v pol'zovatel'skuyu programmu. Metod "zaprosa resursov putem inicializacii" v sovokupnosti s osobymi situaciyami, signaliziruyushchimi ob oshibke, mozhet prigodit'sya pri sozdanii mnogih bibliotek. 9.4.3 Ischerpanie resursa Est' odna iz vechnyh problem programmirovaniya: chto delat', esli ne udalos' udovletvorit' zapros na resurs? Naprimer, v predydushchem primere my spokojno otkryvali s pomoshch'yu fopen() fajly i zaprashivali s pomoshch'yu operacii new blok svobodnoj pamyati, ne zadumyvayas' pri etom, chto takogo fajla mozhet ne byt', a svobodnaya pamyat' mozhet ischerpat'sya. Dlya resheniya takogo roda problem u programmistov est' dva sposoba: Povtornyj zapros: pol'zovatel' dolzhen izmenit' svoj zapros i povtorit' ego. Zavershenie: zaprosit' dopolnitel'nye resursy ot sistemy, esli ih net, zapustit' osobuyu situaciyu. Pervyj sposob predpolagaet dlya zadaniya priemlemogo zaprosa sodejstvie pol'zovatelya, vo vtorom pol'zovatel' dolzhen byt' gotov pravil'no otreagirovat' na otkaz v vydelenii resursov. V bol'shinstve sluchaev poslednij sposob namnogo proshche i pozvolyaet podderzhivat' v sisteme razdelenie razlichnyh urovnej abstrakcii. V S++ pervyj sposob podderzhan mehanizmom vyzova funkcij, a vtoroj - mehanizmom osobyh situacij. Oba sposoba mozhno prodemonstrirovat' na primere realizacii i ispol'zovaniya operacii new: #include <stdlib.h> extern void* _last_allocation; extern void* operator new(size_t size) { void* p; while ( (p=malloc(size))==0 ) { if (_new_handler) (*_new_handler)(); // obratimsya za pomoshch'yu else return 0; } return _last_allocation=p; } Esli operaciya new() ne mozhet najti svobodnoj pamyati, ona obrashchaetsya k upravlyayushchej funkcii _new_handler(). Esli v _new_handler() mozhno vydelit' dostatochnyj ob容m pamyati, vse normal'no. Esli net, iz upravlyayushchej funkcii nel'zya vozvratit'sya v operaciyu new, t.k. vozniknet beskonechnyj cikl. Poetomu upravlyayushchaya funkciya mozhet zapustit' osobuyu situaciyu i predostavit' ispravlyat' polozhenie programme, obrativshejsya k new: void my_new_handler() { try_find_some_memory(); // popytaemsya najti // svobodnuyu pamyat' if (found_some()) return; // esli ona najdena, vse v poryadke throw Memory_exhausted(); // inache zapuskaem osobuyu // situaciyu "Ischerpanie_pamyati" } Gde-to v programme dolzhen byt' proveryaemyj blok s sootvetstvuyushchim obrabotchikom: try { // ... } catch (Memory_exhausted) { // ... } V funkcii operator new() ispol'zovalsya ukazatel' na upravlyayushchuyu funkciyu _new_handler, kotoryj nastraivaetsya standartnoj funkciej set_new_handler(). Esli nuzhno nastroit'sya na sobstvennuyu upravlyayushchuyu funkciyu, nado obratit'sya tak set_new_handler(&my_new_handler); Perehvatit' situaciyu Memory_exhausted mozhno sleduyushchim obrazom: void (*oldnh)() = set_new_handler(&my_new_handler); try { // ... } catch (Memory_exhausted) { // ... } catch (...) { set_new_handler(oldnh); // vosstanovit' ukazatel' na // upravlyayushchuyu funkciyu throw(); // povtornyj zapusk osoboj situacii } set_new_handler(oldnh); // vosstanovit' ukazatel' na // upravlyayushchuyu funkciyu Mozhno postupit' eshche luchshe, esli k upravlyayushchej funkcii primenit' opisannyj v $$9.4 metod "zaprosa resursov putem inicializacii" i ubrat' obrabotchik catch (...). V reshenii, ispol'zuyushchim my_new_handler(), ot tochki obnaruzheniya oshibki do funkcii, v kotoroj ona obrabatyvaetsya, ne peredaetsya nikakoj informacii. Esli nuzhno peredat' kakie-to dannye, to pol'zovatel' mozhet vklyuchit' svoyu upravlyayushchuyu funkciyu v klass. Togda v funkcii, obnaruzhivshej oshibku, nuzhnye dannye mozhno pomestit' v ob容kt etogo klassa. Podobnyj sposob, ispol'zuyushchij ob容kty-funkcii, primenyalsya v $$10.4.2 dlya realizacii manipulyatorov. Sposob, v kotorom ispol'zuetsya ukazatel' na funkciyu ili ob容kt-funkciya dlya togo, chtoby iz upravlyayushchej funkcii, obsluzhivayushchej nekotoryj resurs, proizvesti "obratnyj vyzov" funkcii zaprosivshej etot resurs, obychno nazyvaetsya prosto obratnym vyzovom (callback). Pri etom nuzhno ponimat', chto chem bol'she informacii peredaetsya iz obnaruzhivshej oshibku funkcii v funkciyu, pytayushchuyusya ee ispravit', tem bol'she zavisimost' mezhdu etimi dvumya funkciyami. V obshchem sluchae luchshe svodit' k minimumu takie zavisimosti, poskol'ku vsyakoe izmenenie v odnoj iz funkcij pridetsya delat' s uchetom drugoj funkciej, a, vozmozhno, ee tozhe pridetsya izmenyat'. Voobshche, luchshe ne smeshivat' otdel'nye komponenty programmy. Mehanizm osobyh situacij pozvolyaet sohranyat' razdel'nost' komponentov luchshe, chem obychnyj mehanizm vyzova upravlyayushchih funkcij, kotorye zadaet funkciya, zatrebovavshaya resurs. V obshchem sluchae razumnyj podhod sostoit v tom, chtoby vydelenie resursov bylo mnogourovnevym (v sootvetstvii s urovnyami abstrakcii). Pri etom nuzhno izbegat' togo, chtoby funkcii odnogo urovnya zaviseli ot upravlyayushchej funkcii, vyzyvaemoj na drugom urovne. Opyt sozdaniya bol'shih programmnyh sistem pokazyvaet, chto so vremenem udachnye sistemy razvivayutsya imenno v etom napravlenii. 9.4.4 Osobye situacii i konstruktory Osobye situacii dayut sredstvo signalizirovat' o proishodyashchih v konstruktore oshibkah. Poskol'ku konstruktor ne vozvrashchaet takoe znachenie, kotoroe mogla by proverit' vyzyvayushchaya funkciya, est' sleduyushchie obychnye (t.e. ne ispol'zuyushchie osobye situacii) sposoby signalizacii: [1] Vozvratit' ob容kt v nenormal'nom sostoyanii v raschete, chto pol'zovatel' proverit ego sostoyanie. [2] Ustanovit' znachenie nelokal'noj peremennoj, kotoroe signaliziruet, chto sozdat' ob容kt ne udalos'. Osobye situacii pozvolyayut tot fakt, chto sozdat' ob容kt ne udalos', peredat' iz konstruktora vovne: Vector::Vector(int size) { if (sz<0 || max<sz) throw Size(); // ... } V funkcii, sozdayushchej vektora, mozhno perehvatit' oshibki, vyzvannye nedopustimym razmerom (Size()) i popytat'sya na nih otreagirovat': Vector* f(int i) { Vector* p; try { p = new Vector v(i); } catch (Vector::Size) { // reakciya na nedopustimyj razmer vektora } // ... return p; } Upravlyayushchaya sozdaniem vektora funkciya sposobna pravil'no otreagirovat' na oshibku. V samom obrabotchike osoboj situacii mozhno primenit' kakoj-nibud' iz standartnyh sposobov diagnostiki i vosstanovleniya posle oshibki. Pri kazhdom perehvate osoboj situacii v upravlyayushchej funkcii mozhet byt' svoj vzglyad na prichinu oshibki. Esli s kazhdoj osoboj situaciej peredayutsya opisyvayushchie ee dannye, to ob容m dannyh, kotorye nuzhno analizirovat' dlya kazhdoj oshibki, rastet. Osnovnaya zadacha obrabotki oshibok v tom, chtoby obespechit' nadezhnyj i udobnyj sposob peredachi dannyh ot ishodnoj tochki obnaruzheniya oshibki do togo mesta, gde posle nee vozmozhno osmyslennoe vosstanovlenie. Sposob "zaprosa resursov putem inicializacii" - samyj nadezhnoe i krasivoe reshenie v tom sluchae, kogda imeyutsya konstruktory, trebuyushchie bolee odnogo resursa. Po suti on pozvolyaet svesti zadachu vydeleniya neskol'kih resursov k povtorno primenyaemomu, bolee prostomu, sposobu, rasschitannomu na odin resurs. 9.5 Osobye situacii mogut ne byt' oshibkami Esli osobaya situaciya ozhidalas', byla perehvachena i ne okazala plohogo vozdejstviya na hod programmy, to stoit li ee nazyvat' oshibkoj? Tak govoryat tol'ko potomu, chto programmist dumaet o nej kak ob oshibke, a mehanizm osobyh situacij yavlyaetsya sredstvom obrabotki oshibok. S drugoj storony, osobye situacii mozhno rassmatrivat' prosto kak eshche odnu strukturu upravleniya. Podtverdim eto primerom: class message { /* ... */ }; // soobshchenie class queue { // ochered' // ... message* get(); // vernut' 0, esli ochered' pusta // ... }; void f1(queue& q) { message* m = q.get(); if (m == 0) { // ochered' pusta // ... } // ispol'zuem m } |tot primer mozhno zapisat' tak: class Empty { } // tip osoboj situacii "Pustaya_ochered'" class queue { // ... message* get(); // zapustit' Empty, esli ochered' pusta // ... }; void f2(queue& q) { try { message* m = q.get(); // ispol'zuem m } catch (Empty) { // ochered' pusta // ... } } V variante s osoboj situaciej est' dazhe kakaya-to prelest'. |to horoshij primer togo, kogda trudno skazat', mozhno li schitat' takuyu situaciyu oshibkoj. Esli ochered' ne dolzhna byt' pustoj (t.e. ona byvaet pustoj ochen' redko, skazhem odin raz iz tysyachi), i dejstviya v sluchae pustoj ocheredi mozhno rassmatrivat' kak vosstanovlenie, to v funkcii f2() vzglyad na osobuyu situaciyu budet takoj, kotorogo my do sih por i priderzhivalis' (t.e. obrabotka osobyh situacij est' obrabotka oshibok). Esli ochered' chasto byvaet pustoj, a prinimaemye v etom sluchae dejstviya obrazuyut odnu iz vetvej normal'nogo hoda programmy, to pridetsya otkazat'sya ot takogo vzglyada na osobuyu situaciyu, a funkciyu f2() nado perepisat': class queue { // ... message* get(); // zapustit' Empty, esli ochered' pusta int empty(); // ... }; void f3(queue& q) { if (q.empty()) { // ochered' pusta // ... } else { message* m = q.get(); // ispol'zuem m } } Otmetim, chto vynesti iz funkcii get() proverku ocheredi na pustotu mozhno tol'ko pri uslovii, chto k ocheredi net parallel'nyh obrashchenij. Ne tak to prosto otkazat'sya ot vzglyada, chto obrabotka osoboj situacii est' obrabotka oshibki. Poka my priderzhivaemsya takoj tochki zreniya, programma chetko podrazdelyaetsya na dve chasti: obychnaya chast' i chast' obrabotki oshibok. Takaya programma bolee ponyatna. K sozhaleniyu, v real'nyh zadachah provesti chetkoe razdelenie nevozmozhno, poetomu struktura programmy dolzhna (i budet) otrazhat' etot fakt. Dopustim, ochered' byvaet pustoj tol'ko odin raz (tak mozhet byt', esli funkciya get() ispol'zuetsya v cikle, i pustota ocheredi govorit o konce cikla). Togda pustota ocheredi ne yavlyaetsya chem-to strannym ili oshibochnym. Poetomu, ispol'zuya dlya oboznacheniya konca ocheredi osobuyu situaciyu, my rasshiryaem predstavlenie ob osobyh situaciyah kak oshibkah. S drugoj storony, dejstviya, prinimaemye v sluchae pustoj ocheredi, yavno otlichayutsya ot dejstvij, prinimaemyh v hode cikla (t.e. v obychnom sluchae). Mehanizm osobyh situacij yavlyaetsya menee strukturirovannym, chem takie lokal'nye struktury upravleniya kak operatory if ili for. Obychno on k tomu zhe yavlyaetsya ne stol' effektivnym, esli osobaya situaciya dejstvitel'no voznikla. Poetomu osobye situacii sleduet ispol'zovat' tol'ko v tom sluchae, kogda net horoshego resheniya s bolee tradicionnymi upravlyayushchimi strukturami, ili ono, voobshche, nevozmozhno. Naprimer, v sluchae pustoj ocheredi mozhno prekrasno ispol'zovat' dlya signalizacii ob etom znachenie, a imenno nulevoe znachenie ukazatelya na stroku message, znachit osobaya situaciya zdes' ne nuzhna. Odnako, esli by iz klassa queue my poluchali vmesto ukazatelya znachenie tipa int, to to moglo ne najtis' takogo znacheniya, oboznachayushchego pustuyu ochered'. V takom sluchae funkciya get() stanovitsya ekvivalentnoj operacii indeksacii iz $$9.1, i bolee privlekatel'no predstavlyat' pustuyu ochered' s pomoshch'yu osoboj situacii. Poslednee soobrazhenie podskazyvaet, chto v samom obshchem shablone tipa dlya ocheredi pridetsya dlya oboznacheniya pustoj ocheredi ispol'zovat' osobuyu situaciyu, a rabotayushchaya s ochered'yu funkciya budet takoj: void f(Queue<X>& q) { try { for (;;) { // ``beskonechnyj cikl'' // preryvaemyj osoboj situaciej X m = q.get(); // ... } } catch (Queue<X>::Empty) { return; } } Esli privedennyj cikl vypolnyaetsya tysyachi raz, to on, po vsej vidimosti, budet bolee effektivnym, chem obychnyj cikl s proverkoj usloviya pustoty ocheredi. Esli zhe on vypolnyaetsya tol'ko neskol'ko raz, to obychnyj cikl pochti navernyaka effektivnej. V ocheredi obshchego vida osobaya situaciya ispol'zuetsya kak sposob vozvrata iz funkcii get(). Ispol'zovanie osobyh situacij kak sposoba vozvrata mozhet byt' elegantnym sposobom zaversheniya funkcij poiska. Osobenno eto podhodit dlya rekursivnyh funkcij poiska v dereve. Odnako, primenyaya osobye situacii dlya takih celej, legko perejti gran' razumnogo i poluchit' malovrazumitel'nuyu programmu. Vse-taki vsyudu, gde eto dejstvitel'no opravdano, nado priderzhivat'sya toj tochki zreniya, chto obrabotka osoboj situacii est' obrabotka oshibki. Obrabotka oshibok po samoj svoej prirode zanyatie slozhnoe, poetomu cennost' imeyut lyubye metody, kotorye dayut yasnoe predstavlenie oshibok v yazyke i sposob ih obrabotki. 9.6 Zadanie interfejsa Zapusk ili perehvat osoboj situacii otrazhaetsya na vzaimootnosheniyah funkcij. Poetomu imeet smysl zadavat' v opisanii funkcii mnozhestvo osobyh situacij, kotorye ona mozhet zapustit': void f(int a) throw (x2, x3, x4); V etom opisanii ukazano, chto f() mozhet zapustit' osobye situacii x2, x3 i x4, a takzhe situacii vseh proizvodnyh ot nih tipov, no bol'she nikakie situacii ona ne zapuskaet. Esli funkciya perechislyaet svoi osobye situacii, to ona daet opredelennuyu garantiyu vsyakoj vyzyvayushchej ee funkcii, a imenno, esli popytaetsya zapustit' inuyu osobuyu situaciyu, to eto privedet k vyzovu funkcii unexpected(). Standartnoe prednaznachenie unexpected() sostoit v vyzove funkcii terminate(), kotoraya, v svoyu ochered', obychno vyzyvaet abort(). Podrobnosti dany v $$9.7. Po suti opredelenie void f() throw (x2, x3, x4) { // kakie-to operatory } ekvivalentno takomu opredeleniyu void f() { try { // kakie-to operatory } catch (x2) { // povtornyj zapusk throw; } catch (x3) { // povtornyj zapusk throw; } catch (x4) { // povtornyj zapusk throw; } catch (...) { unexpected(); } } Preimushchestvo yavnogo zadaniya osobyh situacij funkcii v ee opisanii pered ekvivalentnym sposobom, kogda proishodit proverka na osobye situacii v tele funkcii, ne tol'ko v bolee kratkoj zapisi. Glavnoe zdes' v tom, chto opisanie funkcii vhodit v ee interfejs, kotoryj vidim dlya vseh vyzyvayushchih funkcij. S drugoj storony, opredelenie funkcii mozhet i ne byt' universal'no dostupnym. Dazhe esli u vas est' ishodnye teksty vseh bibliotechnyh funkcij, obychno zhelanie izuchat' ih voznikaet ne chasto. Esli v opisanii funkcii ne ukazany ee osobye situacii, schitaetsya, chto ona mozhet zapustit' lyubuyu osobuyu situaciyu. int f(); // mozhet zapustit' lyubuyu osobuyu situaciyu Esli funkciya ne budet zapuskat' nikakih osobyh situacij, ee mozhno opisat', yavno ukazav pustoj spisok: int g() throw (); // ne zapuskaet nikakih osobyh situacij Kazalos' bylo by logichno, chtoby po umolchaniyu funkciya ne zapuskala nikakih osobyh situacij. No togda prishlos' by opisyvat' svoi osobye situacii prakticheski dlya kazhdoj funkcii |to, kak pravilo, trebovalo by ee peretranslyacii, a krome togo prepyatstvovalo by obshcheniyu s funkciyami, napisannymi na drugih yazykah. V rezul'tate programmist stal by stremit'sya otklyuchit' mehanizm osobyh situacij i pisal by izlishnie operatory, chtoby obojti ih. Pol'zovatel' schital by takie programmy nadezhnymi, poskol'ku mog ne zametit' podmeny, no eto bylo by sovershenno neopravdano. 9.6.1 Neozhidannye osobye situacii Esli k opisaniyu osobyh situacij otnosit'sya ne dostatochno ser'ezno, to rezul'tatom mozhet byt' vyzov unexpected(), chto nezhelatel'no vo vseh sluchaya, krome otladki. Izbezhat' vyzova unexpected() mozhno, esli horosho organizovat' strukturu osobyh situacii i opisanie interfejsa. S drugoj storony, vyzov unexpected() mozhno perehvatit' i sdelat' ego bezvrednym. Esli komponent Y horosho razrabotan, vse ego osobye situacii mogut byt' tol'ko proizvodnymi odnogo klassa, skazhem Yerr. Poetomu, esli est' opisanie class someYerr : public Yerr { /* ... */ }; to funkciya, opisannaya kak void f() throw (Xerr, Yerr, IOerr); budet peredavat' lyubuyu osobuyu situaciyu tipa Yerr vyzyvayushchej funkcii. V chastnosti, obrabotka osoboj situacii tipa someYerr v f() svedetsya k peredache ee vyzyvayushchej f() funkcii. Byvaet sluchai, kogda okonchanie programmy pri poyavlenii neozhidannoj osoboj situacii yavlyaetsya slishkom strogim resheniem. Dopustim funkciya g() napisana dlya nesetevogo rezhima v raspredelennoj sisteme. Estestvenno, v g() nichego neizvestno ob osobyh situaciyah, svyazannyh s set'yu, poetomu pri poyavlenii lyuboj iz nih vyzyvaetsya unexpected(). Znachit dlya ispol'zovaniya g() v raspredelennoj sisteme nuzhno predostavit' obrabotchik setevyh osobyh situacij ili perepisat' g(). Esli dopustit', chto perepisat' g() nevozmozhno ili nezhelatel'no, problemu mozhno reshit', pereopredeliv dejstvie funkcii unexpected(). Dlya etogo sluzhit funkciya set_unexpected(). Vnachale my opredelim klass, kotoryj pozvolit nam primenit' dlya funkcij unexpected() metod "zaprosa resursov putem inicializacii" : typedef void(*PFV)(); PFV set_unexpected(PFV); class STC { // klass dlya sohraneniya i vosstanovleniya PFV old; // funkcij unexpected() public: STC(PFV f) { old = set_unexpected(f); } ~STC() { set_unexpected(old); } }; Teper' my opredelim funkciyu, kotoraya dolzhna v nashem primere zamenit' unexpected(): void rethrow() { throw; } // perezapusk vseh setevyh // osobyh situacij Nakonec, mozhno dat' variant funkcii g(), prednaznachennyj dlya raboty v setevom rezhime: void networked_g() { STC xx(&rethrow); // teper' unexpected() vyzyvaet rethrow() g(); } V predydushchem razdele bylo pokazano, chto unexpected() potencial'no vyzyvaetsya iz obrabotchika catch (...). Znachit v nashem sluchae obyazatel'no proizojdet povtornyj zapusk osoboj situacii. Povtornyj zapusk, kogda osobaya situaciya ne zapuskalas', privodit k vyzovu terminate(). Poskol'ku obrabotchik catch (...) nahoditsya vne toj oblasti vidimosti, v kotoroj byla zapushchena setevaya osobaya situaciya, beskonechnyj cikl vozniknut' ne mozhet. Est' eshche odno, dovol'no opasnoe, reshenie, kogda na neozhidannuyu osobuyu situaciyu prosto "zakryvayut glaza": void muddle_on() { cerr << "ne zamechaem osoboj situacii\n"; } // ... STC xx(&muddle_on); // teper' dejstvie unexpected() svoditsya // prosto k pechati soobshcheniya Takoe pereopredelenie dejstviya unexpected() pozvolyaet normal'no vernut'sya iz funkcii, obnaruzhivshej neozhidannuyu osobuyu situaciyu. Nesmotrya na svoyu ochevidnuyu opasnost', eto reshenie ispol'zuetsya. Naprimer, mozhno "zakryt' glaza" na osobye situacii v odnoj chasti sistemy i otlazhivat' drugie ee chasti. Takoj podhod mozhet byt' polezen v processe otladki i razvitiya sistemy, perenesennoj s yazyka programmirovaniya bez osobyh situacij. Vse-taki, kak pravilo luchshe, esli oshibki proyavlyayutsya kak mozhno ran'she. Vozmozhno drugoe reshenie, kogda vyzov unexpected() preobrazuetsya v zapusk osoboj situacii Fail (neudacha): void fail() { throw Fail; } // ... STC yy(&fail); Pri takom reshenii vyzyvayushchaya funkciya ne dolzhna podrobno razbirat'sya v vozmozhnom rezul'tate vyzyvaemoj funkcii: eta funkcii zavershitsya libo uspeshno (t.e. vozvratitsya normal'no), libo neudachno (t.e. zapustit Fail). Ochevidnyj nedostatok etogo resheniya v tom, chto ne uchityvaetsya dopolnitel'naya informaciya, kotoraya mozhet soprovozhdat' osobuyu situaciyu. Vprochem, pri neobhodimosti ee mozhno uchest', esli peredavat' informaciyu vmeste s Fail. 9.7 Neperehvachennye osobye situacii Esli osobaya situaciya zapushchena i ne perehvachena, to vyzyvaetsya funkciya terminate(). Ona zhe vyzyvaetsya, kogda sistema podderzhki osobyh situacij obnaruzhivaet, chto struktura steka narushena, ili kogda v processe obrabotki osoboj situacii pri raskruchivanii steka vyzyvaetsya destruktor, i on pytaetsya zavershit' svoyu rabotu, zapustiv osobuyu situaciyu. Dejstvie terminate() svoditsya k vypolneniyu samoj poslednej funkcii, zadannoj kak parametr dlya set_terminate(): typedef void (*PFV)(); PFV set_terminate(PFV); Funkciya set_terminate() vozvrashchaet ukazatel' na tu funkciyu, kotoraya byla zadana kak parametr v predydushchem obrashchenii k nej. Neobhodimost' takoj funkcii kak terminate() ob座asnyaetsya tem, chto inogda vmesto mehanizma osobyh situacij trebuyutsya bolee grubye priemy. Naprimer, terminate() mozhno ispol'zovat' dlya prekrashcheniya processa, a, vozmozhno, i dlya povtornogo zapuska sistemy. |ta funkciya sluzhit ekstrennym sredstvom, kotoroe primenyaetsya, kogda otkazala strategiya obrabotki oshibok, rasschitannaya na osobye situacii, i samoe vremya primenit' strategiyu bolee nizkogo urovnya. Funkciya unexpected() ispol'zuetsya v shodnyh, no ne stol' ser'eznyh sluchayah, a imenno, kogda funkciya zapustila osobuyu situaciyu, ne ukazannuyu v ee opisanii. Dejstvie funkcii unexpected() svoditsya k vypolneniyu samoj poslednej funkcii, zadannoj kak parametr dlya funkcii set_unexpected(). Po umolchaniyu unexpected() vyzyvaet terminate(), a ta, v svoyu ochered', vyzyvaet funkciyu abort(). Predpolagaetsya, chto takoe soglashenie ustroit bol'shinstvo pol'zovatelej. Predpolagaetsya, chto funkciya terminate() ne vozvrashchaetsya v obrativsheyusya nej funkciyu. Napomnim, chto vyzov abort() svidetel'stvuet o nenormal'nom zavershenii programmy. Dlya normal'nogo vyhoda iz programmy ispol'zuetsya funkciya exit(). Ona vozvrashchaet znachenie, kotoroe pokazyvaet okruzhayushchej sisteme naskol'ko korrektno zakonchilas' programma. 9.8 Drugie sposoby obrabotki oshibok Mehanizm osobyh situacij nuzhen dlya togo, chtoby iz odnoj chasti programmy mozhno bylo soobshchit' v druguyu o vozniknovenii v pervoj "osoboj situacii". Pri etom predpolagaetsya, chto chasti programmy napisany nezavisimo drug ot druga, i v toj chasti, kotoraya obrabatyvaet osobuyu situaciyu, vozmozhna osmyslennaya reakciya na oshibku. Kak zhe dolzhen byt' ustroen obrabotchik osoboj situacii? Privedem neskol'ko variantov: int f(int arg) { try { g(arg); } catch (x1) { // ispravit' oshibku i povtorit' g(arg); } catch (x2) { // proizvesti vychisleniya i vernut' rezul'tat return 2; } catch (x3) { // peredat' oshibku throw; } catch (x4) { // vmesto x4 zapustit' druguyu osobuyu situaciyu throw xxii; } catch (x5) { // ispravit' oshibku i prodolzhit' so sleduyushchego operatora } catch (...) { // otkaz ot obrabotki oshibki terminate(); } // ... } Ukazhem, chto v obrabotchike dostupny peremennye iz oblasti vidimosti, soderzhashchej proveryaemyj blok etogo obrabotchika. Peremennye, opisannye v drugih obrabotchikah ili drugih proveryaemyh blokah, konechno, nedostupny: void f() { int i1; // ... try { int i2; // ... } catch (x1) { int i3; // ... } catch (x4) { i1 = 1; // normal'no i2 = 2; // oshibka: i2 zdes' nevidimo i3 = 3; // oshibka: i3 zdes' nevidimo } } Nuzhna obshchaya strategiya dlya effektivnogo ispol'zovaniya obrabotchikov v programme. Vse komponenty programmy dolzhny soglasovanno ispol'zovat' osobye situacii i imet' obshchuyu chast' dlya obrabotki oshibok. Mehanizm obrabotki osobyh situacij yavlyaetsya nelokal'nym po svoej suti, poetomu tak vazhno priderzhivat'sya obshchej strategii. |to predpolagaet, chto strategiya obrabotki oshibok dolzhna razrabatyvat'sya na samyh rannih stadiyah proektah. Krome togo, eta strategiya dolzhna byt' prostoj (po sravneniyu so slozhnost'yu vsej programmy) i yasnoj. Posledovatel'no provodit' slozhnuyu strategiyu v takoj slozhnoj po svoej prirode oblasti programmirovaniya, kak vosstanovlenie posle oshibok, budet prosto nevozmozhno. Prezhde vsego stoit srazu otkazat'sya ot togo, chto odno sredstvo ili odin priem mozhno primenyat' dlya obrabotki vseh oshibok. |to tol'ko uslozhnit sistemu. Udachnaya sistema, obladayushchaya ustojchivost'yu k oshibkam, dolzhna stroit'sya kak mnogourovnevaya. Na kazhdom urovne nado obrabatyvat' nastol'ko mnogo oshibok, naskol'ko eto vozmozhno bez narusheniya struktury sistemy, ostavlyaya obrabotku drugih oshibok bolee vysokim urovnyam. Naznachenie terminate() podderzhat' takoj podhod, predostavlyaya vozmozhnost' ekstrennogo vyhoda iz takogo polozheniya, kogda narushen sam mehanizm obrabotki osobyh situacij, ili kogda on ispol'zuetsya polnost'yu, no osobaya situaciya okazalas' neperehvachennoj. Funkciya unexpected() prednaznachena dlya vyhoda iz takogo polozheniya, kogda ne srabotalo osnovannoe na opisanii vseh osobyh situacij sredstvo zashchity. |to sredstvo mozhno predstavlyat' kak brandmauer, t.e. stenu, okruzhayushchuyu kazhduyu funkciyu, i prepyatstvuyushchuyu rasprostraneniyu oshibki. Popytka provodit' v kazhdoj funkcii polnyj kontrol', chtoby imet' garantiyu, chto funkciya libo uspeshno zavershitsya, libo zakonchitsya neudachno, no odnim iz opredelennyh i korrektnyh sposobov, ne mozhet prinesti uspeh. Prichiny etogo mogut byt' razlichnymi dlya raznyh programm, no dlya bol'shih programm mozhno nazvat' sleduyushchie: [1] rabota, kotoruyu nuzhno provesti, chtoby garantirovat' nadezhnost' kazhdoj funkcii, slishkom velika, i poetomu ee ne udastsya provesti dostatochno posledovatel'no; [2] poyavyatsya slishkom bol'shie dopolnitel'nye rashody pamyati i vremeni, kotorye budut nedopustimy dlya normal'noj raboty sistemy (budet tendenciya neodnokratno proveryat' na odnu i tu zhe oshibku, a znachit postoyanno budut proveryat'sya peremennye s pravil'nymi znacheniyami); [3] takim ogranicheniyam ne budut podchinyat'sya funkcii, napisannye na drugih yazykah; [4] takoe ponyatie nadezhnosti yavlyaetsya chisto lokal'nym i ono nastol'ko uslozhnyaet sistemu, chto stanovitsya dopolnitel'noj nagruzkoj dlya ee obshchej nadezhnosti. Odnako, razbit' programmu na otdel'nye podsistemy, kotorye libo uspeshno zavershayutsya, libo zakanchivayutsya neudachno, no odnim iz opredelennyh i korrektnyh sposobov, vpolne vozmozhno, vazhno i dazhe vygodno. Takim svojstvom dolzhny obladat' osnovnye biblioteki, podsistemy ili klyuchevye funkcii. Opisanie osobyh situacij dolzhno vhodit' v interfejsy takih bibliotek ili podsistem. Inogda prihoditsya ot odnogo stilya reakcii na oshibku perehodit' na drugoj. Naprimer, mozhno posle vyzova standartnoj funkcii S proveryat' znachenie errno i, vozmozhno, zapuskat' osobuyu situaciyu, a mozhno, naoborot, perehvatyvat' osobuyu situaciyu i ustanavlivat' znachenie errno pered vyhodom iz standartnoj funkcii v S-programmu: void callC() { errno = 0; cfunction(); if (errno) throw some_exception(errno); } void fromC() { try { c_pl_pl_function(); } catch (...) { errno = E_CPLPLFCTBLEWIT; } } Pri takoj smene stilej vazhno byt' posledovatel'nym, chtoby izmenenie reakcii na oshibku bylo polnym. Obrabotka oshibok dolzhna byt', naskol'ko eto vozmozhno, strogo ierarhicheskoj sistemoj. Esli v funkcii obnaruzhena dinamicheskaya oshibka, to ne nuzhno obrashchat'sya za pomoshch'yu dlya vosstanovleniya ili vydeleniya resursov k vyzyvayushchej funkcii. Pri takih obrashcheniyah v strukture sistemy voznikayut ciklicheskie zavisimosti, v rezul'tate chego ee trudnee ponyat', i vozmozhno vozniknovenie beskonechnyh ciklov v processe obrabotki i vosstanovleniya posle oshibki. CHtoby chast' programmy, prednaznachennaya dlya obrabotki oshibok byla bolee uporyadochennoj, stoit primenyat' takie uproshchayushchie delo priemy, kak "zapros resursov putem inicializacii", i ishodit' iz takih uproshchayushchih delo dopushchenij, chto "osobye situacii yavlyayutsya oshibkami". 9.9 Uprazhneniya 1. (*2) Obobshchite klass STC do shablona tipa, kotoryj pozvolyaet hranit' i ustanavlivat' funkcii raznyh tipov. 2. (*3) Dopolnite klass CheckedPtrToT iz $$7.10 do shablona tipa, v kotorom osobye situacii signaliziruyut o dinamicheskih oshibkah. 3. (*3) Napishite funkciyu find dlya poiska v binarnom dereve uzlov po znacheniyu polya tipa char*. Esli najden uzel s polem, imeyushchim znachenie "hello", ona dolzhna vozvrashchat' ukazatel' na nego. Dlya oboznacheniya neudachnogo poiska ispol'zujte osobuyu situaciyu. 4. (*1) Opredelite klass Int, sovpadayushchij vo vsem so vstroennym tipom int za isklyucheniem togo, chto vmesto perepolneniya ili poteri znachimosti v etom klasse zapuskayutsya osobye situacii. Podskazka: sm. $$9.3.2. 5. (*2) Perenesite iz standartnogo interfejsa S v vashu operacionnuyu sistemu osnovnye operacii s fajlami: otkrytie, zakrytie, chtenie i zapis'. Realizujte ih kak funkcii na S++ s tem zhe naznacheniem, chto i funkcij na S, no v sluchae oshibok zapuskajte osobye situacii. 6. (*1) Napishite polnoe opredelenie shablona tipa Vector s osobymi situaciyami Range i Size. Podskazka: sm. $$9.3. 7. (*1) Napishite cikl dlya vychisleniya summy elementov vektora, opredelennogo v uprazhnenii 6, prichem ne proveryajte razmer vektora. Pochemu eto plohoe reshenie? 8. (*2.5) Dopustim klass Exception ispol'zuetsya kak bazovyj dlya vseh klassov, zadayushchih osobye situacii. Kakov dolzhen byt' ego vid? Kakaya ot nego mogla byt' pol'za? Kakie neudobstva mozhet vyzvat' trebovanie obyazatel'nogo ispol'zovaniya etogo klassa? 9. (*2) Napishite klass ili shablon tipa, kotoryj pomozhet realizovat' obratnyj vyzov. 10. (*2) Napishite klass Lock (zamok) dlya kakoj-nibud' sistemy, dopuskayushchej parallel'noe vypolnenie. 11. (*1) Pust' opredelena funkciya int main() { /* ... */ } Izmenite ee tak, chtoby v nej perehvatyvalis' vse osobye situacii, preobrazovyvalis' v soobshcheniya ob oshibke i vyzov abort(). Podskazka: v funkcii fromC() iz $$9.8 uchteny ne vse sluchai.  * GLAVA 10. POTOKI "Dostupno tol'ko to, chto vidimo" B. Kernigan V yazyke S++ net sredstv dlya vvoda-vyvoda. Ih i ne nuzhno, poskol'ku takie sredstva mozhno prosto i elegantno sozdat' na samom yazyke. Opisannaya zdes' biblioteka potokovogo vvoda-vyvoda realizuet strogij tipovoj i vmeste s tem gibkij i effektivnyj sposob simvol'nogo vvoda i vyvoda celyh, veshchestvennyh chisel i simvol'nyh strok, a takzhe yavlyaetsya bazoj dlya rasshireniya, rasschitannogo na rabotu s pol'zovatel'skimi tipami dannyh. Pol'zovatel'skij interfejs biblioteki nahoditsya v fajle <iostream.h>. |ta glava posvyashchena samoj potokovoj biblioteke, nekotorym sposobam raboty s nej i opredelennym priemam realizacii biblioteki. 10.1 VVEDENIE SHiroko izvestna trudnost' zadachi proektirovaniya i realizacii standartnyh sredstv vvoda-vyvoda dlya yazykov programmirovaniya. Tradicionno sredstva vvoda-vyvoda byli rasschitany isklyuchitel'no na nebol'shoe chislo vstroennyh tipov dannyh. Odnako, v netrivial'nyh programmah na S++ est' mnogo pol'zovatel'skih tipov dannyh, poetomu neobhodimo predostavit' vozmozhnost' vvoda-vyvoda znachenij takih tipov. Ochevidno, chto sredstva vvoda-vyvoda dolzhny byt' prostymi, udobnymi, nadezhnymi v ispol'zovanii i, chto vazhnee vsego, adekvatnymi. Poka nikto ne nashel resheniya, kotoroe udovletvorilo by vseh; poetomu neobhodimo dat' vozmozhnost' pol'zovatelyu sozdavat' inye sredstva vvoda-vyvoda, a takzhe rasshiryat' standartnye sredstva vvoda-vyvoda v raschete na opredelennoe primenenie. Ce