void put(T); // vyzvat' overflow(T), kogda bufer polon T get(); // vyzvat' underflow(T), kogda bufer pust }; template<class T> class circular_buffer : public buffer<T> { //... int overflow(T); // perejti na nachalo bufera, esli on polon int underflow(); }; template<class T> class expanding_buffer : public buffer<T> { //... int overflow(T); // uvelichit' razmer bufera, esli on polon int underflow(); }; |tot metod ispol'zovalsya v bibliotekah potokovogo vvoda-vyvoda ($$10.5.3). 12.2.4 Otnosheniya prinadlezhnosti Esli ispol'zuetsya otnoshenie prinadlezhnosti, to sushchestvuet dva osnovnyh sposoba predstavleniya ob容kta klassa X: [1] Opisat' chlen tipa X. [2] Opisat' chlen tipa X* ili X&. Esli znachenie ukazatelya ne budet menyat'sya i voprosy effektivnosti ne volnuyut, eti sposoby ekvivalentny: class X { //... public: X(int); //... }; class C { X a; X* p; public: C(int i, int j) : a(i), p(new X(j)) { } ~C() { delete p; } }; V takih situaciyah predpochtitel'nee neposredstvennoe chlenstvo ob容kta, kak X::a v primere vyshe, potomu chto ono daet ekonomiyu vremeni, pamyati i kolichestva vvodimyh simvolov. Obratites' takzhe k $$12.4 i $$13.9. Sposob, ispol'zuyushchij ukazatel', sleduet primenyat' v teh sluchayah, kogda prihoditsya perestraivat' ukazatel' na "ob容kt-element" v techenii zhizni "ob容kta-vladel'ca". Naprimer: class C2 { X* p; public: C(int i) : p(new X(i)) { } ~C() { delete p; } X* change(X* q) { X* t = p; p = q; return t; } }; CHlen tipa ukazatel' mozhet takzhe ispol'zovat'sya, chtoby dat' vozmozhnost' peredavat' "ob容kt-element" v kachestve parametra: class C3 { X* p; public: C(X* q) : p(q) { } // ... } Razreshaya ob容ktam soderzhat' ukazateli na drugie ob容kty, my sozdaem to, chto obychno nazyvaetsya "ierarhiya ob容ktov". |to al'ternativnyj i vspomogatel'nyj sposob strukturirovaniya po otnosheniyu k ierarhii klassov. Kak bylo pokazano na primere avarijnogo dvizhushchegosya sredstva v $$12.2.2, chasto eto dovol'no tonkij vopros proektirovaniya: predstavlyat' li svojstvo klassa kak eshche odin bazovyj klass ili kak chlen klassa. Potrebnost' v pereopredelenii sleduet schitat' ukazaniem, chto pervyj variant luchshe. No esli nado imet' vozmozhnost' predstavlyat' nekotoroe svojstvo s pomoshch'yu razlichnyh tipov, to luchshe ostanovit'sya na vtorom variante. Naprimer: class XX : public X { /*...*/ }; class XXX : public X { /*...*/ }; void f() { C3* p1 = new C3(new X); // C3 "soderzhit" X C3* p2 = new C3(new XX); // C3 "soderzhit" XX C3* p3 = new C3(new XXX); // C3 "soderzhit" XXX //... } Privedennye opredeleniya nel'zya smodelirovat' ni s pomoshch'yu proizvodnogo klassa C3 ot X, ni s pomoshch'yu C3, imeyushchego chlen tipa X, poskol'ku neobhodimo ukazyvat' tochnyj tip chlena. |to vazhno dlya klassov s virtual'nymi funkciyami, takih, naprimer,kak klass Shape ($$1.1.2.5), i dlya klassa abstraktnogo mnozhestva ($$13.3). Zametim, chto ssylki mozhno primenyat' dlya uproshcheniya klassov, ispol'zuyushchih chleny-ukazateli, esli v techenie zhizni ob容kta-vladel'ca ssylka nastroena tol'ko na odin ob容kt, naprimer: class C4 { X& r; public: C(X& q) : r(q) { } // ... }; 12.2.5 Prinadlezhnost' i nasledovanie Uchityvaya slozhnost' vazhnost' otnoshenij nasledovaniya, net nichego udivitel'nogo v tom, chto chasto ih nepravil'no ponimayut i ispol'zuyut sverh mery. Esli klass D opisan kak obshchij proizvodnyj ot klassa B, to chasto govoryat, chto D est' B: class B { /* ... */ ; class D : public B /* ... */ }; // D sorta B Inache eto mozhno sformulirovat' tak: nasledovanie - eto otnoshenie "est'", ili, bolee tochno dlya klassov D i B, nasledovanie - eto otnoshenie D sorta B. V otlichie ot etogo, esli klass D soderzhit v kachestve chlena drugoj klass B, to govoryat, chto D "imeet" B: class D { // D imeet B // ... public: B b; // ... }; Inymi slovami, prinadlezhnost' - eto otnoshenie "imet'" ili dlya klassov D i B prosto: D soderzhit B. Imeya dva klassa B i D, kak vybirat' mezhdu nasledovaniem i prinadlezhnost'yu? Rassmotrim klassy samolet i motor.Novichki obychno sprashivayut: budet li horoshim resheniem sdelat' klass samolet proizvodnym ot klassa motor. |to plohoe reshenie, poskol'ku samolet ne "est'" motor, samolet "imeet" motor. Sleduet podojti k etomu voprosu, rassmotrev, mozhet li samolet "imet'" dva ili bol'she motorov. Poskol'ku eto predstavlyaetsya vpolne vozmozhnym (dazhe esli my imeem delo s programmoj, v kotoroj vse samolety budut s odnim motorom), sleduet ispol'zovat' prinadlezhnost', a ne nasledovanie. Vopros "Mozhet li on imet' dva..?" okazyvaetsya udivitel'no poleznym vo mnogih somnitel'nyh sluchayah. Kak vsegda, nashe izlozhenie zatragivaet neulovimuyu sushchnost' programmirovaniya. Esli by vse klassy bylo tak zhe legko predstavit', kak samolet i motor, to bylo by prosto izbezhat' i trivial'nyh oshibok tipa toj, kogda samolet opredelyaetsya kak proizvodnoe ot klassa motor. Odnako, takie oshibki dostatochno chasty, osobenno u teh, kto schitaet nasledovanie eshche odnim mehanizmom dlya sochetaniya konstrukcij yazyka programmirovaniya. Nesmotrya na udobstvo i lakonichnost' zapisi, kotoruyu predostavlyaet nasledovanie, ego nado ispol'zovat' tol'ko dlya vyrazheniya teh otnoshenij, kotorye chetko opredeleny v proekte. Rassmotrim opredeleniya: class B { public: virtual void f(); void g(); }; class D1 { // D1 soderzhit B public: B b; void f(); // ne pereopredelyaet b.f() }; void h1(D1* pd) { B* pb = pd; // oshibka: nevozmozhno preobrazovanie D1* v B* pb = &pd->b; pb->q(); // vyzov B::q pd->q(); // oshibka: D1 ne imeet chlen q() pd->b.q(); pb->f(); // vyzov B::f (zdes' D1::f ne pereopredelyaet) pd->f(); // vyzov D1::f } Obratite vnimanie, chto v etom primere net neyavnogo preobrazovaniya klassa k odnomu iz ego elementov, i chto klass, soderzhashchij v kachestve chlena drugoj klass, ne pereopredelyaet virtual'nye funkcii etogo chlena. Zdes' yavnoe otlichie ot primera, privedennogo nizhe: class D2 : public B { // D2 est' B public: void f(); // pereopredelenie B::f() }; void h2(D2* pd) { B* pb = pd; // normal'no: D2* neyavno preobrazuetsya v B* pb->q(); // vyzov B::q pd->q(); // vyzov B::q pb->f(); // vyzov virtual'noj funkcii: obrashchenie k D2::f pd->f(); // vyzov D2::f } Udobstvo zapisi, prodemonstrirovannoe v primere s klassom D2, po sravneniyu s zapis'yu v primere s klassom D1, yavlyaetsya prichinoj, po kotoroj takim nasledovaniem zloupotreblyayut. No sleduet pomnit', chto sushchestvuet opredelennaya plata za udobstvo zapisi v vide vozrosshej zavisimosti mezhdu B i D2 (sm. $$12.2.3). V chastnosti, legko zabyt' o neyavnom preobrazovanii D2 v B. Esli tol'ko takie preobrazovaniya ne otnosyatsya k semantike vashih klassov, sleduet izbegat' opisaniya proizvodnogo klassa v obshchej chasti. Esli klass predstavlyaet opredelennoe ponyatie, a nasledovanie ispol'zuetsya kak otnoshenie "est'", to takie preobrazovaniya obychno kak raz to, chto nuzhno. Odnako, byvayut takie situacii, kogda zhelatel'no imet' nasledovanie, no nel'zya dopuskat' preobrazovaniya. Rassmotrim zadanie klassa cfield (controled field - upravlyaemoe pole), kotoryj, pomimo vsego prochego, daet vozmozhnost' kontrolirovat' na stadii vypolneniya dostup k drugomu klassu field. Na pervyj vzglyad kazhetsya sovershenno pravil'nym opredelit' klass cfield kak proizvodnyj ot klassa field: class cfield : public field { // ... }; |to vyrazhaet tot fakt, chto cfield, dejstvitel'no, est' sorta field, uproshchaet zapis' funkcii, kotoraya ispol'zuet chlen chasti field klassa cfield, i, chto samoe glavnoe, pozvolyaet v klasse cfield pereopredelyat' virtual'nye funkcii iz field. Zagvozdka zdes' v tom, chto preobrazovanie cfield* k field*, vstrechayushcheesya v opredelenii klassa cfield, pozvolyaet obojti lyuboj kontrol' dostupa k field: void q(cfield* p) { *p = "asdf"; // obrashchenie k field kontroliruetsya // funkciej prisvaivaniya cfield: // p->cfield::operator=("asdf") field* q = p; // neyavnoe preobrazovanie cfield* v field* *q = "asdf"; // priehali! kontrol' obojden } Mozhno bylo by opredelit' klass cfield tak, chtoby field byl ego chlenom, no togda cfield ne mozhet pereopredelyat' virtual'nye funkcii field. Luchshim resheniem zdes' budet ispol'zovanie nasledovaniya so specifikaciej private (chastnoe nasledovanie): class cfield : private field { /* ... */ } S pozicii proektirovaniya, esli ne uchityvat' (inogda vazhnye) voprosy pereopredeleniya, chastnoe nasledovanie ekvivalentno prinadlezhnosti. V etom sluchae primenyaetsya metod, pri kotorom klass opredelyaetsya v obshchej chasti kak proizvodnyj ot abstraktnogo bazovogo klassa zadaniem ego interfejsa, a takzhe opredelyaetsya s pomoshch'yu chastnogo nasledovaniya ot konkretnogo klassa, zadayushchego realizaciyu ($$13.3). Poskol'ku nasledovanie, ispol'zuemoe kak chastnoe, yavlyaetsya specifikoj realizacii, i ono ne otrazhaetsya v tipe proizvodnogo klassa, to ego inogda nazyvayut "nasledovaniem po realizacii", i ono yavlyaetsya kontrastom dlya nasledovaniya v obshchej chasti, kogda nasleduetsya interfejs bazovogo klassa i dopustimy neyavnye preobrazovaniya k bazovomu tipu. Poslednee nasledovanie inogda nazyvayut opredeleniem podtipa ili "interfejsnym nasledovaniem". Dlya dal'nejshego obsuzhdeniya vozmozhnosti vybora nasledovaniya ili prinadlezhnosti rassmotrim, kak predstavit' v dialogovoj graficheskoj sisteme svitok (oblast' dlya prokruchivaniya v nej informacii), i kak privyazat' svitok k oknu na ekrane. Potrebuyutsya svitki dvuh vidov: gorizontal'nye i vertikal'nye. |to mozhno predstavit' s pomoshch'yu dvuh tipov horizontal_scrollbar i vertical_scrollbar ili s pomoshch'yu odnogo tipa scrollbar, kotoryj imeet argument, opredelyayushchij, yavlyaetsya raspolozhenie vertikal'nym ili gorizontal'nym. Pervoe reshenie predpolagaet, chto est' eshche tretij tip, zadayushchij prosto svitok - scrollbar, i etot tip yavlyaetsya bazovym klassom dlya dvuh opredelennyh svitkov. Vtoroe reshenie predpolagaet dopolnitel'nyj argument u tipa scrollbar i nalichie znachenij, zadayushchih vid svitka. Naprimer, tak: enum orientation { horizontal, vertical }; Kak tol'ko my ostanovimsya na odnom iz reshenij, opredelitsya ob容m izmenenij, kotorye pridetsya vnesti v sistemu. Dopustim, v etom primere nam potrebuetsya vvesti svitki tret'ego vida. Vnachale predpolagalos', chto mogut byt' svitki tol'ko dvuh vidov (ved' vsyakoe okno imeet tol'ko dva izmereniya), no v etom primere, kak i vo mnogih drugih, vozmozhny rasshireniya, kotorye voznikayut kak voprosy pereproektirovaniya. Naprimer, mozhet poyavit'sya zhelanie ispol'zovat' "upravlyayushchuyu knopku" (tipa myshi) vmesto svitkov dvuh vidov. Takaya knopka zadavala by prokrutku v razlichnyh napravleniyah v zavisimosti ot togo, v kakoj chasti okna nazhal ee pol'zovatel'. Nazhatie v seredine verhnej strochki dolzhno vyzyvat' "prokruchivanie vverh", nazhatie v seredine levogo stolbca - "prokruchivanie vlevo", nazhatie v levom verhnem uglu - "prokruchivanie vverh i vlevo". Takaya knopka ne yavlyaetsya chem-to neobychnym, i ee mozhno rassmatrivat' kak utochnenie ponyatiya svitka, kotoroe osobenno podhodit dlya teh oblastej prilozheniya, kotorye svyazany ne s obychnymi tekstami, a s bolee slozhnoj informaciej. Dlya dobavleniya upravlyayushchej knopki k programme, ispol'zuyushchej ierarhiyu iz treh svitkov, trebuetsya dobavit' eshche odin klass, no ne nuzhno menyat' programmu, rabotayushchuyu so starymi svitkami: svitok gorizontal'nyj_svitok vertikal'nyj_svitok upravlyayushchaya_knopka |to polozhitel'naya storona "ierarhicheskogo resheniya". Zadanie orientacii svitka v kachestve parametra privodit k zadaniyu polej tipa v ob容ktah svitka i ispol'zovaniyu pereklyuchatelej v tele funkcij-chlenov svitka. Inymi slovami, pered nami obychnaya dilemma: vyrazit' dannyj aspekt struktury sistemy s pomoshch'yu opredelenij ili realizovat' ego v operatornoj chasti programmy. Pervoe reshenie uvelichivaet ob容m staticheskih proverok i ob容m informacii, nad kotoroj mogut rabotat' raznye vspomogatel'nye sredstva. Vtoroe reshenie otkladyvaet proverki na stadiyu vypolneniya i razreshaet menyat' tela otdel'nyh funkcij, ne izmenyaya obshchuyu strukturu sistemy, kakoj ona predstavlyaetsya s tochki zreniya staticheskogo kontrolya ili vspomogatel'nyh sredstv. V bol'shinstve sluchaev, predpochtitel'nee pervoe reshenie. Polozhitel'noj storonoj resheniya s edinym tipom svitka yavlyaetsya to, chto legko peredavat' informaciyu o vide nuzhnogo nam svitka drugoj funkcii: void helper(orientation oo) { //... p = new scrollbar(oo); //... } void me() { helper(horizontal); } Takoj podhod pozvolyaet na stadii vypolneniya legko perenastroit' svitok na druguyu orientaciyu. Vryad li eto ochen' vazhno v primere so svitkami, no eto mozhet okazat'sya sushchestvennym v pohozhih primerah. Sut' v tom, chto vsegda nado delat' opredelennyj vybor, a eto chasto neprosto. Teper' rassmotrim kak privyazat' svitok k oknu. Esli schitat' window_with_scrollbar (okno_so_svitkom) kak nechto, chto yavlyaetsya window i scrollbar, my poluchim podobnoe: class window_with_scrollbar : public window, public scrollbar { // ... }; |to pozvolyaet lyubomu ob容ktu tipa window_with_scrollbar vystupat' i kak window, i kak scrollbar, no ot nas trebuetsya reshenie ispol'zovat' tol'ko edinstvennyj tip scrollbar. Esli, s drugoj storony, schitat' window_with_scrollbar ob容ktom tipa window, kotoryj imeet scrollbar, my poluchim takoe opredelenie: class window_with_scrollbar : public window { // ... scrollbar* sb; public: window_with_scrollbar(scrollbar* p, /* ... */) : window(/* ... */), sb(p) { // ... } // ... }; Zdes' my mozhem ispol'zovat' reshenie so svitkami treh tipov. Peredacha samogo svitka v kachestve parametra pozvolyaet oknu (window) ne zapominat' tip ego svitka. Esli potrebuetsya, chtoby ob容kt tipa window_with_scrollbar dejstvoval kak scrollbar, mozhno dobavit' operaciyu preobrazovaniya: window_with_scrollbar :: operator scrollbar&() { return *sb; } 12.2.6 Otnosheniya ispol'zovaniya Dlya sostavleniya i ponimaniya proekta chasto neobhodimo znat', kakie klassy i kakim sposobom ispol'zuet dannyj klass. Takie otnosheniya klassov na S++ vyrazhayutsya neyavno. Klass mozhet ispol'zovat' tol'ko te imena, kotorye gde-to opredeleny, no net takoj chasti v programme na S++, kotoraya soderzhala by spisok vseh ispol'zuemyh imen. Dlya polucheniya takogo spiska neobhodimy vspomogatel'nye sredstva (ili, pri ih otsutstvii, vnimatel'noe chtenie). Mozhno sleduyushchim obrazom klassificirovat' te sposoby, s pomoshch'yu kotoryh klass X mozhet ispol'zovat' klass Y: - X ispol'zuet imya Y - X ispol'zuet Y - X vyzyvaet funkciyu-chlen Y - X chitaet chlen Y - X pishet v chlen Y - X sozdaet Y - X razmeshchaet auto ili static peremennuyu iz Y - X sozdaet Y s pomoshch'yu new - X ispol'zuet razmer Y My otnesli ispol'zovanie razmera ob容kta k ego sozdaniyu, poskol'ku dlya etogo trebuetsya znanie polnogo opredeleniya klassa. S drugoj storony, my vydelili v otdel'noe otnoshenie ispol'zovanie imeni Y, poskol'ku, ukazyvaya ego v opisanii Y* ili v opisanii vneshnej funkcii, my vovse ne nuzhdaemsya v dostupe k opredeleniyu Y: class Y; // Y - imya klassa Y* p; extern Y f(const Y&); My otdelili sozdanie Y s pomoshch'yu new ot sluchaya opisaniya peremennoj, poskol'ku vozmozhna takaya realizaciya S++, pri kotoroj dlya sozdaniya Y s pomoshch'yu new neobyazatel'no znat' razmer Y. |to mozhet byt' sushchestvenno dlya ogranicheniya vseh zavisimostej v proekte i svedeniya k minimumu peretranslyacii posle vneseniya izmenenij. YAzyk S++ ne trebuet, chtoby sozdatel' klassov tochno opredelyal, kakie klassy i kak on budet ispol'zovat'. Odna iz prichin etogo zaklyuchena v tom, chto samye vazhnye klassy zavisyat ot stol' bol'shogo kolichestva drugih klassov, chto dlya pridaniya luchshego vida programme nuzhna sokrashchennaya forma zapisi spiska ispol'zuemyh klassov, naprimer, s pomoshch'yu komandy #include. Drugaya prichina v tom, chto klassifikaciya etih zavisimostej i, v chastnosti, ob枸dinenie nekotoryh zavisimostej ne yavlyaetsya obyazannost'yu yazyka programmirovaniya. Naoborot, celi razrabotchika, programmista ili vspomogatel'nogo sredstva opredelyayut to, kak imenno sleduet rassmatrivat' otnosheniya ispol'zovaniya. Nakonec, to, kakie zavisimosti predstavlyayut bol'shij interes, mozhet zaviset' ot specifiki realizacii yazyka. 12.2.7 Otnosheniya vnutri klassa Do sih por my obsuzhdali tol'ko klassy, i hotya operacii upominalis', esli ne schitat' obsuzhdeniya shagov processa razvitiya programmnogo obespecheniya ($$11.3.3.2), to oni byli na vtorom plane, ob容kty zhe prakticheski voobshche ne upominalis'. Ponyat' eto prosto: v S++ klass, a ne funkciya ili ob容kt, yavlyaetsya osnovnym ponyatiem organizacii sistemy. Klass mozhet skryvat' v sebe vsyakuyu specifiku realizacii, naravne s "gryaznymi" priemami programmirovaniya, a inogda on vynuzhden eto delat'. V to zhe vremya ob容kty bol'shinstva klassov sami obrazuyut regulyarnuyu strukturu i ispol'zuyutsya takimi sposobami, chto ih dostatochno prosto opisat'. Ob容kt klassa mozhet byt' sovokupnost'yu drugih vlozhennyh ob容ktov (ih chasto nazyvayut chlenami), mnogie iz kotoryh, v svoyu ochered', yavlyayutsya ukazatelyami ili ssylkami na drugie ob容kty. Poetomu otdel'nyj ob容kt mozhno rassmatrivat' kak koren' dereva ob容ktov, a vse vhodyashchie v nego ob容kty kak "ierarhiyu ob容ktov", kotoraya dopolnyaet ierarhiyu klassov, rassmotrennuyu v $$12.2.4. Rassmotrim v kachestve primera klass strok iz $$7.6: class String { int sz; char* p; public: String(const char* q); ~String(); //... }; Ob容kt tipa String mozhno izobrazit' tak: 12.2.7.1 Invarianty Znachenie chlenov ili ob容ktov, dostupnyh s pomoshch'yu chlenov klassa, nazyvaetsya sostoyaniem ob容kta (ili prosto znacheniem ob容kta). Glavnoe pri postroenii klassa - eto: privesti ob容kt v polnost'yu opredelennoe sostoyanie (inicializaciya), sohranyat' polnost'yu opredelennoe sostoyanie ob枸kta v processe vypolneniya nad nim razlichnyh operacij, i v konce raboty unichtozhit' ob容kt bez vsyakih posledstvij. Svojstvo, kotoroe delaet sostoyanie ob容kta polnost'yu opredelennym, nazyvaetsya invariantom. Poetomu naznachenie inicializacii - zadat' konkretnye znacheniya, pri kotoryh vypolnyaetsya invariant ob容kta. Dlya kazhdoj operacii klassa predpolagaetsya, chto invariant dolzhen imet' mesto pered vypolneniem operacii i dolzhen sohranit'sya posle operacii. V konce raboty destruktor narushaet invariant, unichtozhaya ob容kt. Naprimer, konstruktor String::String(const char*) garantiruet, chto p ukazyvaet na massiv iz, po krajnej mere, sz elementov, prichem sz imeet osmyslennoe znachenie i v[sz-1]==0. Lyubaya strokovaya operaciya ne dolzhna narushat' eto utverzhdenie. Pri proektirovanii klassa trebuetsya bol'shoe iskusstvo, chtoby sdelat' realizaciyu klassa dostatochno prostoj i dopuskayushchej nalichie poleznyh invariantov, kotorye neslozhno zadat'. Legko trebovat', chtoby klass imel invariant, trudnee predlozhit' poleznyj invariant, kotoryj ponyaten i ne nakladyvaet zhestkih ogranichenij na dejstviya razrabotchika klassa ili na effektivnost' realizacii. Zdes' "invariant" ponimaetsya kak programmnyj fragment, vypolniv kotoryj, mozhno proverit' sostoyanie ob容kta. Vpolne vozmozhno dat' bolee strogoe i dazhe matematicheskoe opredelenie invarianta, i v nekotoryh situaciyah ono mozhet okazat'sya bolee podhodyashchim. Zdes' zhe pod invariantom ponimaetsya prakticheskaya, a znachit, obychno ekonomnaya, no nepolnaya proverka sostoyaniya ob容kta. Ponyatie invarianta poyavilos' v rabotah Flojda, Naura i Hora, posvyashchennyh pred- i post-usloviyam, ono vstrechaetsya vo vseh vazhnyh stat'yah po abstraktnym tipam dannyh i verifikacii programm za poslednie 20 let. Ono zhe yavlyaetsya osnovnym predmetom otladki v C++. Obychno, v techenie raboty funkcii-chlena invariant ne sohranyaetsya. Poetomu funkcii, kotorye mogut vyzyvat'sya v te momenty, kogda invariant ne dejstvuet, ne dolzhny vhodit' v obshchij interfejs klassa. Takie funkcii dolzhny byt' chastnymi ili zashchishchennymi. Kak mozhno vyrazit' invariant v programme na S++? Prostoe reshenie - opredelit' funkciyu, proveryayushchuyu invariant, i vstavit' vyzovy etoj funkcii v obshchie operacii. Naprimer: class String { int sz; int* p; public: class Range {}; class Invariant {}; void check(); String(const char* q); ~String(); char& operator[](int i); int size() { return sz; } //... }; void String::check() { if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz-1]) throw Invariant; } char& String::operator[](int i) { check(); // proverka na vhode if (i<0 || i<sz) throw Range; // dejstvuet check(); // proverka na vyhode return v[i]; } |tot variant prekrasno rabotaet i ne oslozhnyaet zhizn' programmista. No dlya takogo prostogo klassa kak String proverka invarianta budet zanimat' bol'shuyu chast' vremeni scheta. Poetomu programmisty obychno vypolnyayut proverku invarianta tol'ko pri otladke: inline void String::check() { if (!NDEBUG) if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz]) throw Invariant; } My vybrali imya NDEBUG, poskol'ku eto makroopredelenie, kotoroe ispol'zuetsya dlya analogichnyh celej v standartnom makroopredelenii S assert(). Tradicionno NDEBUG ustanavlivaetsya s cel'yu ukazat', chto otladki net. Ukazav, chto check() yavlyaetsya podstanovkoj, my garantirovali, chto nikakaya programma ne budet sozdana, poka konstanta NDEBUG ne budet ustanovlena v znachenie, oboznachayushchee otladku. S pomoshch'yu shablona tipa Assert() mozhno zadat' menee regulyarnye utverzhdeniya, naprimer: template<class T, class X> inline void Assert(T expr,X x) { if (!NDEBUG) if (!expr) throw x; } vyzovet osobuyu situaciyu x, esli expr lozhno, i my ne otklyuchili proverku s pomoshch'yu NDEBUG. Ispol'zovat' Assert() mozhno tak: class Bad_f_arg { }; void f(String& s, int i) { Assert(0<=i && i<s.size(),Bad_f_arg()); //... } SHablon tipa Assert() podrazhaet makrokomande assert() yazyka S. Esli i ne nahoditsya v trebuemom diapazone, voznikaet osobaya situaciya Bad_f_arg. S pomoshch'yu otdel'noj konstanty ili konstanty iz klassa proverit' podobnye utverzhdeniya ili invarianty - pustyakovoe delo. Esli zhe neobhodimo proverit' invarianty s pomoshch'yu ob容kta, mozhno opredelit' proizvodnyj klass, v kotorom proveryayutsya operaciyami iz klassa, gde net proverki, sm. upr.8 v $$13.11. Dlya klassov s bolee slozhnymi operaciyami rashody na proverki mogut byt' znachitel'ny, poetomu proverki mozhno ostavit' tol'ko dlya "poimki" trudno obnaruzhivaemyh oshibok. Obychno polezno ostavlyat' po krajnej mere neskol'ko proverok dazhe v ochen' horosho otlazhennoj programme. Pri vseh usloviyah sam fakt opredeleniya invariantov i ispol'zovaniya ih pri otladke daet neocenimuyu pomoshch' dlya polucheniya pravil'noj programmy i, chto bolee vazhno, delaet ponyatiya, predstavlennye klassami, bolee regulyarnymi i strogo opredelennymi. Delo v tom, chto kogda vy sozdaete invarianty, to rassmatrivaete klass s drugoj tochki zreniya i vnosite opredelennuyu izbytochnost' v programmu. To i drugoe uvelichivaet veroyatnost' obnaruzheniya oshibok, protivorechij i nedosmotrov. My ukazali v $$11.3.3.5, chto dve samye obshchie formy preobrazovaniya ierarhii klassov sostoyat v razbienii klassa na dva i v vydelenii obshchej chasti dvuh klassov v bazovyj klass. V oboih sluchayah horosho produmannyj invariant mozhet podskazat' vozmozhnost' takogo preobrazovaniya. Esli, sravnivaya invariant s programmami operacij, mozhno obnaruzhit', chto bol'shinstvo proverok invarianta izlishni, to znachit klass sozrel dlya razbieniya. V etom sluchae podmnozhestvo operacij imeet dostup tol'ko k podmnozhestvu sostoyanij ob容kta. Obratno, klassy sozreli dlya sliyaniya, esli u nih shodnye invarianty, dazhe pri nekotorom razlichii v ih realizacii. 12.2.7.2 Inkapsulyaciya Otmetim, chto v S++ klass, a ne otdel'nyj ob容kt, yavlyaetsya toj edinicej, kotoraya dolzhna byt' inkapsulirovana (zaklyuchena v obolochku). Naprimer: class list { list* next; public: int on(list*); }; int list::on(list* p) { list* q = this; for(;;) { if (p == q) return 1; if (q == 0) return 0; q = q->next; } } Zdes' obrashchenie k chastnomu ukazatelyu list::next dopustimo, poskol'ku list::on() imeet dostup ko vsyakomu ob容ktu klassa list, na kotoryj u nego est' ssylka. Esli eto neudobno, situaciyu mozhno uprostit', otkazavshis' ot vozmozhnosti dostupa cherez funkciyu-chlen k predstavleniyam drugih ob容ktov, naprimer: int list::on(list* p) { if (p == this) return 1; if (p == 0) return 0; return next->on(p); } No teper' iteraciya prevrashchaetsya v rekursiyu, chto mozhet sil'no zamedlit' vypolnenie programmy, esli tol'ko translyator ne sumeet obratno preobrazovat' rekursiyu v iteraciyu. 12.2.8 Programmiruemye otnosheniya Konkretnyj yazyk programmirovaniya ne mozhet pryamo podderzhivat' lyuboe ponyatie lyubogo metoda proektirovaniya. Esli yazyk programmirovaniya ne sposoben pryamo predstavit' ponyatie proektirovaniya, sleduet ustanovit' udobnoe otobrazhenie konstrukcij, ispol'zuemyh v proekte, na yazykovye konstrukcii. Naprimer, metod proektirovaniya mozhet ispol'zovat' ponyatie delegirovaniya, oznachayushchee, chto vsyakaya operaciya, kotoraya ne opredelena dlya klassa A, dolzhna vypolnyat'sya v nem s pomoshch'yu ukazatelya p na sootvetstvuyushchij chlen klassa B, v kotorom ona opredelena. Na S++ nel'zya vyrazit' eto pryamo. Odnako, realizaciya etogo ponyatiya nastol'ko v duhe S++, chto legko predstavit' programmu realizacii: class A { B* p; //... void f(); void ff(); }; class B { //... void f(); void g(); void h(); }; Tot fakt, chto V delegiruet A s pomoshch'yu ukazatelya A::p, vyrazhaetsya v sleduyushchej zapisi: class A { B* p; // delegirovanie s pomoshch'yu p //... void f(); void ff(); void g() { p->g(); } // delegirovanie q() void h() { p->h(); } // delegirovanie h() }; Dlya programmista sovershenno ochevidno, chto zdes' proishodit, odnako zdes' yavno narushaetsya princip vzaimnoodnoznachnogo sootvetstviya. Takie "programmiruemye" otnosheniya trudno vyrazit' na yazykah programmirovaniya, i poetomu k nim trudno primenyat' razlichnye vspomogatel'nye sredstva. Naprimer, takoe sredstvo mozhet ne otlichit' "delegirovanie" ot B k A s pomoshch'yu A::p ot lyubogo drugogo ispol'zovaniya B*. Vse-taki sleduet vsyudu, gde eto vozmozhno, dobivat'sya vzaimnoodnoznachnogo sootvetstviya mezhdu ponyatiyami proekta i ponyatiyami yazyka programmirovaniya. Ono daet opredelennuyu prostotu i garantiruet, chto proekt adekvatno otobrazhaetsya v programme, chto uproshchaet rabotu programmista i vspomogatel'nyh sredstv. Operacii preobrazovanij tipa yavlyayutsya mehanizmom, s pomoshch'yu kotorogo mozhno predstavit' v yazyke klass programmiruemyh otnoshenij, a imenno: operaciya preobrazovaniya X::operator Y() garantiruet, chto vsyudu, gde dopustimo ispol'zovanie Y, mozhno primenyat' i X. Takoe zhe otnoshenie zadaet konstruktor Y::Y(X). Otmetim, chto operaciya preobrazovaniya tipa (kak i konstruktor) skoree sozdaet novyj ob容kt, chem izmenyaet tip sushchestvuyushchego ob容kta. Zadat' operaciyu preobrazovaniya k funkcii Y - oznachaet prosto potrebovat' neyavnogo primeneniya funkcii, vozvrashchayushchej Y. Poskol'ku neyavnye primeneniya operacij preobrazovaniya tipa i operacij, opredelyaemyh konstruktorami, mogut privesti k nepriyatnostyam, polezno proanalizirovat' ih v otdel'nosti eshche v proekte. Vazhno ubedit'sya, chto graf primenenij operacij preobrazovaniya tipa ne soderzhit ciklov. Esli oni est', voznikaet dvusmyslennaya situaciya, pri kotoroj tipy, uchastvuyushchie v ciklah, stanovyatsya nesovmestimymi v kombinacii. Naprimer: class Big_int { //... friend Big_int operator+(Big_int,Big_int); //... operator Rational(); //... }; class Rational { //... friend Rational operator+(Rational,Rational); //... operator Big_int(); }; Tipy Rational i Big_int ne tak gladko vzaimodejstvuyut, kak mozhno bylo by podumat': void f(Rational r, Big_int i) { //... g(r+i); // oshibka, neodnoznachnost': // operator+(r,Rational(i)) ili // operator+(Big_int(r),i) g(r,Rational(i)); // yavnoe razreshenie neopredelennosti g(Big_int(r),i); // eshche odno } Mozhno bylo by izbezhat' takih "vzaimnyh" preobrazovanij, sdelav nekotorye iz nih yavnymi. Naprimer, preobrazovanie Big_int k tipu Rational mozhno bylo by zadat' yavno s pomoshch'yu funkcii make_Rational() vmesto operacii preobrazovaniya, togda slozhenie v privedennom primere razreshalos' by kak g(BIg_int(r),i). Esli nel'zya izbezhat' "vzaimnyh" operacij preobrazovaniya tipov, to nuzhno preodolevat' voznikayushchie stolknoveniya ili s pomoshch'yu yavnyh preobrazovanij (kak bylo pokazano), ili s pomoshch'yu opredeleniya neskol'kih razlichnyh versij binarnoj operacii (v nashem sluchae +). 12.3 Komponenty V yazyke S++ net konstrukcij, kotorye mogut vyrazit' pryamo v programme ponyatie komponenta, t.e. mnozhestva svyazannyh klassov. Osnovnaya prichina etogo v tom, chto mnozhestvo klassov (vozmozhno s sootvetstvuyushchimi global'nymi funkciyami i t.p.) mozhet soedinyat'sya v komponent po samym raznym priznakam. Otsutstvie yavnogo predstavleniya ponyatiya v yazyke zatrudnyaet provedenie granicy mezhdu informaciej (imena), ispol'zuemoj vnutri komponenta, i informaciej (imena), peredavaemoj iz komponenta pol'zovatelyam. V ideale, komponent opredelyaetsya mnozhestvom interfejsov, ispol'zuemyh dlya ego realizacii, plyus mnozhestvom interfejsov, predstavlyaemyh pol'zovatelem, a vse prochee schitaetsya "specifikoj realizacii" i dolzhno byt' skryto ot ostal'nyh chastej sistemy. Takovo mozhet byt' v dejstvitel'nosti predstavlenie o komponente u razrabotchika. Programmist dolzhen smirit'sya s tem faktom, chto S++ ne daet obshchego ponyatiya prostranstva imen komponenta, tak chto ego prihoditsya "modelirovat'" s pomoshch'yu ponyatij klassov i edinic translyacii, t.e. teh sredstv, kotorye est' v S++ dlya ogranicheniya oblasti dejstviya nelokal'nyh imen. Rassmotrim dva klassa, kotorye dolzhny sovmestno ispol'zovat' funkciyu f() i peremennuyu v. Proshche vsego opisat' f i v kak global'nye imena. Odnako, vsyakij opytnyj programmist znaet, chto takoe "zasorenie" prostranstva imen mozhet privesti v konce koncov k nepriyatnostyam: kto-to mozhet nenarochno ispol'zovat' imena f ili v ne po naznacheniyu ili narochno obratit'sya k f ili v, pryamo ispol'zuya "specifiku realizacii" i obojdya tem samym yavnyj interfejs komponenta. Zdes' vozmozhny tri resheniya: [1] Dat' "neobychnye" imena ob容ktam i funkciyam, kotorye ne rasschitany na pol'zovatelya. [2] Ob容kty ili funkcii, ne prednaznachennye dlya pol'zovatelya, opisat' v odnom iz fajlov programmy kak staticheskie (static). [3] Pomestit' ob容kty i funkcii, ne prednaznachennye dlya pol'zovatelya, v klass, opredelenie kotorogo zakryto dlya pol'zovatelej. Pervoe reshenie primitivno i dostatochno neudobno dlya sozdatelya programmy, no ono dejstvuet: // ne ispol'zujte specifiku realizacii compX, // esli tol'ko vy ne razrabotchik compX: extern void compX_f(T2*, const char*); extern T3 compX_v; // ... Takie imena kak compX_f i compX_v vryad li mogut privesti k kollizii, a na tot dovod, chto pol'zovatel' mozhet byt' zloumyshlennikom i ispol'zovat' eti imena pryamo, mozhno otvetit', chto pol'zovatel' v lyubom sluchae mozhet okazat'sya zloumyshlennikom, i chto yazykovye mehanizmy zashchity predohranyayut ot neschastnogo sluchaya, a ne ot zlogo umysla. Preimushchestvo etogo resheniya v tom, chto ono primenimo vsegda i horosho izvestno. V to zhe vremya ono nekrasivo, nenadezhno i uslozhnyaet vvod teksta. Vtoroe reshenie bolee nadezhno, no menee universal'no: // specifika realizacii compX: static void compX_f(T2* a1, const char *a2) { /* ... */ } static T3 compX_v; // ... Trudno garantirovat', chto informaciya, ispol'zuemaya v klassah odnogo komponenta, budet dostupna tol'ko v odnoj edinice translyacii, poskol'ku operacii, rabotayushchie s etoj informaciej, dolzhny byt' dostupny vezde. |to reshenie mozhet k tomu zhe privesti k gromadnym edinicam translyacii, a v nekotoryh otladchikah dlya S++ ne organizovan dostup k imenam staticheskih funkcij i peremennyh. V to zhe vremya eto reshenie nadezhno i chasto optimal'no dlya nebol'shih komponentov. Tret'e reshenie mozhno rassmatrivat' kak formalizaciyu i obobshchenie pervyh dvuh: class compX_details { // specifika realizacii compX public: static void f(T2*, const char*); static T3 v; // ... }; Opisanie compX_details budet ispol'zovat' tol'ko sozdatel' klassa, ostal'nye ne dolzhny vklyuchat' ego v svoi programmy. V komponente konechno mozhet byt' mnogo klassov, ne prednaznachennyh dlya obshchego pol'zovaniya. Esli ih imena tozhe rasschitany tol'ko na lokal'noe ispol'zovanie, to ih takzhe mozhno "spryatat'" vnutri klassov, soderzhashchih specifiku realizacii: class compX_details { // specifika realizacii compX. public: // ... class widget { // ... }; // ... }; Ukazhem, chto vlozhennost' sozdaet bar'er dlya ispol'zovaniya widget v drugih chastyah programmy. Obychno klassy, predstavlyayushchie yasnye ponyatiya, schitayutsya pervymi kandidatami na povtornoe ispol'zovanie, i, znachit sostavlyayut chast' interfejsa komponenta, a ne detal' realizacii. Drugimi slovami, hotya dlya sohraneniya nadlezhashchego urovnya abstrakcii vlozhennye ob容kty, ispol'zuemye dlya predstavleniya nekotorogo ob容kta klassa, luchshe schitat' skrytymi detalyami realizacii, klassy, opredelyayushchie takie vlozhennye ob容kty, luchshe ne delat' skrytymi, esli oni imeyut dostatochnuyu obshchnost'. Tak, v sleduyushchem primere upryatyvanie, pozhaluj, izlishne: class Car { class Wheel { // ... }; Wheel flw, frw, rlw, rrw; // ... }; Vo mnogih situaciyah dlya podderzhaniya urovnya abstrakcii ponyatiya mashiny (Car) sleduet upryatyvat' real'nye kolesa (klass Wheel), ved' kogda vy rabotaete s mashinoj, vy ne mozhete nezavisimo ot nee ispol'zovat' kolesa. S drugoj storony, sam klass Wheel yavlyaetsya vpolne podhodyashchim dlya shirokogo ispol'zovaniya, poetomu luchshe vynesti ego opredelenie iz klassa Car: class Wheel { // ... }; class Car { Wheel flw, frw, rlw, rrw; // ... }; Ispol'zovat' li vlozhennost'? Otvet na etot vopros zavisit ot celej proekta i obshchnosti ispol'zuemyh ponyatij. Kak vlozhennost', tak i ee otsutstvie mogut byt' vpolne dopustimymi resheniyami dlya dannogo proekta. No poskol'ku vlozhennost' predohranyaet ot zasoreniya obshchego prostranstva imen, v svode pravil nizhe rekomenduetsya ispol'zovat' vlozhennost', esli tol'ko net prichin ne delat' etogo. Otmetim, chto zagolovochnye fajly dayut moshchnoe sredstvo dlya razlichnyh predstavlenij komponent raznym pol'zovatelyam, i oni zhe pozvolyayut udalyat' iz predstavleniya komponenta dlya pol'zovatelya te klassy, kotorye svyazany so specifikoj realizacii. Drugim sredstvom postroeniya komponenta i predstavleniya ego pol'zovatelyu sluzhit ierarhiya. Togda bazovyj klass ispol'zuetsya kak hranilishche obshchih dannyh i funkcij. Takim sposobom ustranyaetsya problema, svyazannaya s global'nymi dannymi i funkciyami, prednaznachennymi dlya realizacii obshchih zaprosov klassov dannogo komponenta. S drugoj storony, pri takom reshenii klassy komponenta stanovyatsya slishkom svyazannymi drug s drugom, a pol'zovatel' popadaet v zavisimost' ot vseh bazovyh klassov teh komponentov, kotorye emu dejstvitel'no nuzhny. Zdes' takzhe proyavlyaetsya tendenciya k tomu, chto chleny, predstavlyayushchie "poleznye" funkcii i dannye "vsplyvayut" k bazovomu klassu, tak chto pri slishkom bol'shoj ierarhii klassov problemy s global'nymi dannymi i funkciyami proyavyatsya uzhe v ramkah etoj ierarhii. Veroyatnee vsego, eto proizojdet dlya ierarhii s odnim kornem, a dlya bor'by s etim yavleniem mozhno primenyat' virtual'nye bazovye klassy ($$6.5.4). Inogda luchshe vybrat' ierarhiyu dlya predstavleniya komponenta, a inogda net. Kak vsegda sdelat' vybor predstoit razrabotchiku. 12.4 Interfejsy i realizacii Ideal'nyj interfejs dolzhen - predstavlyat' polnoe i soglasovannoe mnozhestvo ponyatij dlya pol'zovatelya, - byt' soglasovannym dlya vseh chastej komponenta, - skryvat' specifiku realizacii ot pol'zovatelya, - dopuskat' neskol'ko realizacij, - imet' staticheskuyu sistemu tipov, - opredelyat'sya s pomoshch'yu tipov iz oblasti prilozheniya, - zaviset' ot drugih interfejsov lish' chastichno i vpolne opredelennym obrazom. Otmetiv neobhodimost' soglasovannosti dlya vseh klassov, kotorye obrazuyut interfejs komponenta s ostal'nym mirom, my mozhem uprostit' vopros interfejsa, rassmotrev tol'ko odin klass, naprimer: class X { // primer plohogo opredeleniya interfejsa Y a; Z b; public: void f(const char* ...); void g(int[],int); void set_a(Y&); Y& get_a(); }; V etom interfejse soderzhitsya ryad potencial'nyh problem: -Tipy Y i Z ispol'zuyutsya tak, chto opredeleniya Y i Z dolzhny byt' izvestny vo vremya translyacii. - U funkcii X::f mozhet byt' proizvol'noe chislo parametrov neizvestnogo tipa (vozmozhno, oni kakim-to obrazom kontroliruyutsya "strokoj formata", kotoraya peredaetsya v kachestve pervogo parametra). - Funkciya X::g imeet parametr tipa int[]. Vozmozhno eto normal'no, no obychno eto svidetel'stvuet o tom, chto opredelenie slishkom nizkogo urovnya abstrakcii. Massiv celyh ne yavlyaetsya dostatochnym opredeleniem, tak kak neizvestno iz skol'kih on mozhet sostoyat' elementov. - Funkcii set_a() i get_a(), po vsej vidimosti, raskryvayut predstavlenie ob容ktov klassa X, razreshaya pryamoj dostup k X::a. Zdes' funkcii-chleny obrazuyut interfejs na slishkom nizkom urovne abstrakcii. Kak pravilo klassy s interfejsom takogo urovnya otnosyatsya k specifike realizacii bol'shogo komponenta, esli oni voobshche mogut k chemu-nibud' otnosit'sya. V ideale parametr funkcii iz interfejsa dolzhen soprovozhdat'sya takoj informaciej, kotoroj dostatochno dlya ego ponimaniya. Mozhno sformulirovat' takoe pravilo: nado umet' peredavat' zaprosy na obsluzhivanie udalennomu serveru po uzkomu kanalu. YAzyk S++ raskryvaet predstavlenie klassa kak chast' interfejsa. |to predstavlenie mozhet byt' skrytym (s pomoshch'yu private ili protected), no obyazatel'no dostupnym translyatoru, chtoby on mog razmestit' avtomaticheskie (lokal'nye) peremennye, sdelat' podstanovku tela funkcii i t.d. Otricatel'nym sledstviem etogo yavlyaetsya to, chto ispol'zovanie tipov klassov v predstavlenii klassa mozhet privesti k vozniknoveniyu nezhelatel'nyh zavisimostej. Privedet li ispol'zovanie chlenov tipa Y i Z k problemam, zavisit ot togo, kakovy v dejstvitel'nosti tipy Y i Z. Esli eto dostatochno prostye tipy, napodobie complex ili String, to ih ispol'zovanie budet vpolne dopustimym v bol'shinstve sluchaev. Takie tipy mozhno schitat' ustojchivymi, i neobhodimost' vklyuchat' opredeleniya ih klassov budet vpolne dopustimoj nagruzkoj dlya translyatora. Esli zhe Y i Z sami yavlyayutsya klassami interfejsa bol'shogo komponenta (naprimer, tipa graficheskoj sistemy ili sistemy obespecheniya bankovskih schetov), to pryamuyu zavisimost' ot nih mozhno schitat' nerazumnoj. V takih sluchayah predpochtitel'nee ispol'zovat' chlen, yavlyayushchijsya ukazatelem ili ssylkoj: class X { Y* a; Z& b; // ... }; Pri etom sposobe opredelenie X otdelyaetsya ot opredelenij Y i Z, t.e. teper' opredelenie X zavisit tol'ko ot imen Y i Z. Realizaciya X, konechno, budet po-prezhnemu zaviset' ot opredelenij Y i Z, no eto uzhe ne budet okazyvat' neblagopriyatnogo vliyaniya na pol'zovatelej X. Vysheskazannoe illyustriruet vazhnoe utverzhdenie: U interfejsa, skryvayushchego znachitel'nyj ob容m informacii (chto i dolzhen delat' poleznyj interfejs), dolzhno byt' sushchestvenno men'she zavisimostej, chem u realizacii, kotoraya ih skryvaet. Naprimer, opredelenie klassa X mozhno translirovat' bez dostupa k opredeleniyam Y i Z. Odnako, v opredeleniyah funkcij-chlenov klassa X, kotorye rabotayut so ssylkami na ob容kty Y i Z, dostup k opredeleniyam Y i Z neobhodim. Pri analize zavisimostej sleduet rassmatrivat' razdel'no zavisimosti v interfejse i v realizacii. V ideale dlya oboih vidov zavisimostej graf zavisimostej sistemy dolzhen byt' napravlennym neciklichnym grafom, chto oblegchaet ponimanie i testirovanie sistemy. Odnako, eta cel' bo