ym sredstvom, postavlyayushchim informaciyu o tipe, mozhno zadat' s pomoshch'yu sleduyushchih operacij: typeid static_type_info(type) // poluchit' typeid dlya imeni tipa typeid ptr_type_info(pointer) // poluchit' typeid dlya ukazatelya typeid ref_type_info(reference) // poluchit' typeid dlya ssylki pointer ptr_cast(type,pointer) // preobrazovanie ukazatelya reference ref_cast(type,reference) // preobrazovanie ssylki Pol'zovatel' klassa mozhet obojtis' etimi operaciyami, a sozdatel' klassa dolzhen predusmotret' v opisaniyah klassov opredelennye "prisposobleniya", chtoby soglasovat' operacii s realizaciej biblioteki. Bol'shinstvo pol'zovatelej, kotorym voobshche nuzhna dinamicheskaya identifikaciya tipa, mozhet ogranichit'sya operaciyami privedeniya ptr_cast() i ref_cast(). Takim obrazom pol'zovatel' otstranyaetsya ot dal'nejshih slozhnostej, svyazannyh s dinamicheskoj identifikaciej tipa. Krome togo, ogranichennoe ispol'zovanie dinamicheskoj informacii o tipe men'she vsego chrevato oshibkami. Esli nedostatochno znat', chto operaciya privedeniya proshla uspeshno, a nuzhen istinnyj tip (naprimer, ob容ktno-orientirovannyj vvod-vyvod), to mozhno ispol'zovat' operacii dinamicheskih zaprosov o tipe: static_type_info(), ptr_type_info() i ref_type_info(). |ti operacii vozvrashchayut ob容kt klassa typeid. Kak bylo pokazano v primere s set i slist_set, ob容kty klassa typeid mozhno sravnivat'. Dlya bol'shinstva zadach etih svedenij o klasse typeid dostatochno. No dlya zadach, kotorym nuzhna bolee polnaya informaciya o tipe, v klasse typeid est' funkciya get_type_info(): class typeid { friend class Type_info; private: const Type_info* id; public: typeid(const Type_info* p) : id(p) { } const Type_info* get_type_info() const { return id; } int operator==(typeid i) const ; }; Funkciya get_type_info() vozvrashchaet ukazatel' na nemenyayushchijsya (const) ob容kt klassa Type_info iz typeid. Sushchestvenno, chto ob容kt ne menyaetsya: eto dolzhno garantirovat', chto dinamicheskaya informaciya o tipe otrazhaet staticheskie tipy ishodnoj programmy. Ploho, esli pri vypolnenii programmy nekotoryj tip mozhet izmenyat'sya. S pomoshch'yu ukazatelya na ob容kt klassa Type_info pol'zovatel' poluchaet dostup k informacii o tipe iz typeid i, teper' ego programma nachinaet zaviset' ot konkretnoj sistemy dinamicheskih zaprosov o tipe i ot struktury dinamicheskoj informacii o nem. No eti sredstva ne vhodyat v standart yazyka, a zadat' ih s pomoshch'yu horosho produmannyh makroopredelenij neprosto. 13.5.2 Klass Type_info V klasse Type_info est' minimal'nyj ob容m informacii dlya realizacii operacii ptr_cast(); ego mozhno opredelit' sleduyushchim obrazom: class Type_info { const char* n; // imya const Type_info** b; // spisok bazovyh klassov public: Type_info(const char* name, const Type_info* base[]); const char* name() const; Base_iterator bases(int direct=0) const; int same(const Type_info* p) const; int has_base(const Type_info*, int direct=0) const; int can_cast(const Type_info* p) const; static const Type_info info_obj; virtual typeid get_info() const; static typeid info(); }; Dve poslednie funkcii dolzhny byt' opredeleny v kazhdom proizvodnom ot Type_info klasse. Pol'zovatel' ne dolzhen zabotit'sya o strukture ob容kta Type_info, i ona privedena zdes' tol'ko dlya polnoty izlozheniya. Stroka, soderzhashchaya imya tipa, vvedena dlya togo, chtoby dat' vozmozhnost' poiska informacii v tablicah imen, naprimer, v tablice otladchika. S pomoshch'yu nee a takzhe informacii iz ob容kta Type_info mozhno vydavat' bolee osmyslennye diagnosticheskie soobshcheniya. Krome togo, esli vozniknet potrebnost' imet' neskol'ko ob容ktov tipa Type_info, to imya mozhet sluzhit' unikal'nym klyuchom etih ob容ktov. const char* Type_info::name() const { return n; } int Type_info::same(const Type_info* p) const { return this==p || strcmp(n,p->n)==0; } int Type_info::can_cast(const Type_info* p) const { return same(p) || p->has_base(this); } Dostup k informacii o bazovyh klassah obespechivaetsya funkciyami bases() i has_base(). Funkciya bases() vozvrashchaet iterator, kotoryj porozhdaet ukazateli na bazovye klassy ob容ktov Type_info, a s pomoshch'yu funkcii has_base() mozhno opredelit' yavlyaetsya li zadannyj klass bazovym dlya drugogo klassa. |ti funkcii imeyut neobyazatel'nyj parametr direct, kotoryj pokazyvaet, sleduet li rassmatrivat' vse bazovye klassy (direct=0), ili tol'ko pryamye bazovye klassy (direct=1). Nakonec, kak opisano nizhe, s pomoshch'yu funkcij get_info() i info() mozhno poluchit' dinamicheskuyu informaciyu o tipe dlya samogo klassa Type_info. Zdes' sredstvo dinamicheskih zaprosov o tipe soznatel'no realizuetsya s pomoshch'yu sovsem prostyh klassov. Tak mozhno izbezhat' privyazki k opredelennoj biblioteke. Realizaciya v raschete na konkretnuyu biblioteku mozhet byt' inoj. Mozhno, kak vsegda, posovetovat' pol'zovatelyam izbegat' izlishnej zavisimosti ot detalej realizacii. Funkciya has_base() ishchet bazovye klassy s pomoshch'yu imeyushchegosya v Type_info spiska bazovyh klassov. Hranit' informaciyu o tom, yavlyaetsya li bazovyj klass chastnym ili virtual'nym, ne nuzhno, poskol'ku vse oshibki, svyazannye s ogranicheniyami dostupa ili neodnoznachnost'yu, budut vyyavleny pri translyacii. class base_iterator { short i; short alloc; const Type_info* b; public: const Type_info* operator() (); void reset() { i = 0; } base_iterator(const Type_info* bb, int direct=0); ~base_iterator() { if (alloc) delete[] (Type_info*)b; } }; V sleduyushchem primere ispol'zuetsya neobyazatel'nyj parametr dlya ukazaniya, sleduet li rassmatrivat' vse bazovye klassy (direct==0) ili tol'ko pryamye bazovye klassy (direct==1). base_iterator::base_iterator(const Type_info* bb, int direct) { i = 0; if (direct) { // ispol'zovanie spiska pryamyh bazovyh klassov b = bb; alloc = 0; return; } // sozdanie spiska pryamyh bazovyh klassov: // int n = chislo bazovyh b = new const Type_info*[n+1]; // zanesti bazovye klassy v b alloc = 1; return; } const Type_info* base_iterator::operator() () { const Type_info* p = &b[i]; if (p) i++; return p; } Teper' mozhno zadat' operacii zaprosov o tipe s pomoshch'yu makroopredelenij: #define static_type_info(T) T::info() #define ptr_type_info(p) ((p)->get_info()) #define ref_type_info(r) ((r).get_info()) #define ptr_cast(T,p) \ (T::info()->can_cast((p)->get_info()) ? (T*)(p) : 0) #define ref_cast(T,r) \ (T::info()->can_cast((r).get_info()) \ ? 0 : throw Bad_cast(T::info()->name()), (T&)(r)) Predpolagaetsya, chto tip osoboj situacii Bad_cast (Oshibka_privedeniya) opisan tak: class Bad_cast { const char* tn; // ... public: Bad_cast(const char* p) : tn(p) { } const char* cast_to() { return tn; } // ... }; V razdele $$4.7 bylo skazano, chto poyavlenie makroopredelenij sluzhit signalom voznikshih problem. Zdes' problema v tom, chto tol'ko translyator imeet neposredstvennyj dostup k literal'nym tipam, a makroopredeleniya skryvayut specifiku realizacii. Po suti dlya hraneniya informacii dlya dinamicheskih zaprosov o tipah prednaznachena tablica virtual'nyh funkcij. Esli realizaciya neposredstvenno podderzhivaet dinamicheskuyu identifikaciyu tipa, to rassmatrivaemye operacii mozhno realizovat' bolee estestvenno, effektivno i elegantno. V chastnosti, ochen' prosto realizovat' funkciyu ptr_cast(), kotoraya preobrazuet ukazatel' na virtual'nyj bazovyj klass v ukazatel' na ego proizvodnye klassy. 13.5.3 Kak sozdat' sistemu dinamicheskih zaprosov o tipe Zdes' pokazano, kak mozhno pryamo realizovat' dinamicheskie zaprosy o tipe, kogda v translyatore takih vozmozhnostej net. |to dostatochno utomitel'naya zadacha i mozhno propustit' etot razdel, tak kak v nem est' tol'ko detali konkretnogo resheniya. Klassy set i slist_set iz $$13.3 sleduet izmenit' tak, chtoby s nimi mogli rabotat' operacii zaprosov o tipe. Prezhde vsego, v bazovyj klass set nuzhno vvesti funkcii-chleny, kotorye ispol'zuyut operacii zaprosov o tipe: class set { public: static const Type_info info_obj; virtual typeid get_info() const; static typeid info(); // ... }; Pri vypolnenii programmy edinstvennym predstavitelem ob容kta tipa set yavlyaetsya set::info_obj, kotoryj opredelyaetsya tak: const Type_info set::info_obj("set",0); S uchetom etogo opredeleniya funkcii trivial'ny: typeid set::get_info() const { return &info_obj; } typeid set::info() { return &info_obj; } typeid slist_set::get_info() const { return &info_obj; } typeid slist_set::info() { return &info_obj; } Virtual'naya funkciya get_info() budet predostavlyat' operacii ref_type_info() i ptr_type_info(), a staticheskaya funkciya info() - operaciyu static_type_info(). Pri takom postroenii sistemy zaprosov o tipe osnovnaya trudnost' na praktike sostoit v tom, chtoby dlya kazhdogo klassa ob容kt tipa Type_info i dve funkcii, vozvrashchayushchie ukazatel' na etot ob容kt, opredelyalis' tol'ko odin raz. Nuzhno neskol'ko izmenit' klass slist_set: class slist_set : public set, private slist { // ... public: static const Type_info info_obj; virtual typeid get_info() const; static typeid info(); // ... }; static const Type_info* slist_set_b[] = { &set::info_obj, &slist::info_obj, 0 }; const Type_info slist_set::info_obj("slist_set",slist_set_b); typeid slist_set::get_info() const { return &info_obj; } typeid slist_set::info() { return &info_obj; } 13.5.4 Rasshirennaya dinamicheskaya informaciya o tipe V klasse Type_info soderzhitsya tol'ko minimum informacii, neobhodimoj dlya identifikacii tipa i bezopasnyh operacij privedeniya. No poskol'ku v samom klasse Type_info est' funkcii-chleny info() i get_info(), mozhno postroit' proizvodnye ot nego klassy, chtoby v dinamike opredelyat', kakie ob容kty Type_info vozvrashchayut eti funkcii. Takim obrazom, ne menyaya klassa Type_info, pol'zovatel' mozhet poluchat' bol'she informacii o tipe s pomoshch'yu ob容ktov, vozvrashchaemyh funkciyami dynamic_type() i static_type(). Vo mnogih sluchayah dopolnitel'naya informaciya dolzhna soderzhat' tablicu chlenov ob容kta: struct Member_info { char* name; Type_info* tp; int offset; }; class Map_info : public Type_info { Member_info** mi; public: static const Type_info info_obj; virtual typeid get_info() const; static typeid info(); // funkcii dostupa }; Klass Type_info vpolne podhodit dlya standartnoj biblioteki. |to bazovyj klass s minimumom neobhodimoj informacii, iz kotorogo mozhno poluchat' proizvodnye klassy, predostavlyayushchie bol'she informacii. |ti proizvodnye klassy mogut opredelyat' ili sami pol'zovateli, ili kakie-to sluzhebnye programmy, rabotayushchie s tekstom na S++, ili sami translyatory yazyka. 13.5.5 Pravil'noe i nepravil'noe ispol'zovanie dinamicheskoj informacii o tipe Dinamicheskaya informaciya o tipe mozhet ispol'zovat'sya vo mnogih situaciyah, v tom chisle dlya: ob容ktnogo vvoda-vyvoda, ob容ktno-orientirovannyh baz dannyh, otladki. V tozhe vremya velika veroyatnost' oshibochnogo ispol'zovaniya takoj informacii. Izvestno,chto v yazyke Simula ispol'zovanie takih sredstv, kak pravilo, privodit k oshibkam. Poetomu eti sredstva ne byli vklyucheny v S++. Slishkom velik soblazn vospol'zovat'sya dinamicheskoj informaciej o tipe, togda kak pravil'nee vyzvat' virtual'nuyu funkciyu. Rassmotrim v kachestve primera klass Shape iz $$1.2.5. Funkciyu rotate mozhno bylo zadat' tak: void rotate(const Shape& s) // nepravil'noe ispol'zovanie dinamicheskoj // informacii o tipe { if (ref_type_info(s)==static_type_info(Circle)) { // dlya etoj figury nichego ne nado } else if (ref_type_info(s)==static_type_info(Triangle)) { // vrashchenie treugol'nika } else if (ref_type_info(s)==static_type_info(Square)) { // vrashchenie kvadrata } // ... } Esli dlya pereklyuchatelya po tipu polya my ispol'zuem dinamicheskuyu informaciyu o tipe, to tem samym narushaem v programme princip modul'nosti i otricaem sami celi ob容ktno-orientirovannogo programmirovaniya. K tomu zhe eto reshenie chrevato oshibkami: esli v kachestve parametra funkcii budet peredan ob容kt proizvodnogo ot Circle klassa, to ona srabotaet neverno (dejstvitel'no, vrashchat' krug (Circle) net smysla, no dlya ob容kta, predstavlyayushchego proizvodnyj klass, eto mozhet potrebovat'sya). Opyt pokazyvaet, chto programmistam, vospitannym na takih yazykah kak S ili Paskal', trudno izbezhat' etoj lovushki. Stil' programmirovaniya etih yazykov trebuet men'she predusmotritel'nosti, a pri sozdanii biblioteki takoj stil' mozhno prosto schitat' nebrezhnost'yu. Mozhet vozniknut' vopros, pochemu v interfejs s sistemoj dinamicheskoj informacii o tipe vklyuchena uslovnaya operaciya privedeniya ptr_cast(), a ne operaciya is_base(), kotoraya neposredstvenno opredelyaetsya s pomoshch'yu operacii has_base() iz klassa Type_info. Rassmotrim takoj primer: void f(dialog_box& db) { if (is_base(&db,dbox_w_str)) { // yavlyaetsya li db bazovym // dlya dbox_w-str? dbox_w_str* dbws = (dbox_w_str*) &db; // ... } // ... } Reshenie s pomoshch'yu ptr_cast ($$13.5) bolee korotkoe, k tomu zhe zdes' yavnaya i bezuslovnaya operaciya privedeniya otdelena ot proverki v operatore if, znachit poyavlyaetsya vozmozhnost' oshibki, neeffektivnosti i dazhe nevernogo rezul'tata. Nevernyj rezul'tat mozhet vozniknut' v teh redkih sluchayah, kogda sistema dinamicheskoj identifikacii tipa raspoznaet, chto odin tip yavlyaetsya proizvodnym ot drugogo, no translyatoru etot fakt neizvesten, naprimer: class D; class B; void g(B* pb) { if (is_base(pb,D)) { D* pb = (D*)pb; // ... } // ... } Esli translyatoru poka neizvestno sleduyushchee opisanie klassa D: class D : public A, public B { // ... }; to voznikaet oshibka, t.k. pravil'noe privedenie ukazatelya pb k D* trebuet izmeneniya znacheniya ukazatelya. Reshenie s operaciej ptr_cast() ne stalkivaetsya s etoj trudnost'yu, poskol'ku eta operaciya primenima tol'ko pri uslovii, chto v oblasti vidimosti nahodyatsya opisaniya obeih ee parametrov. Privedennyj primer pokazyvaet, chto operaciya privedeniya dlya neopisannyh klassov po suti svoej nenadezhna, no zapreshchenie ee sushchestvenno uhudshaet sovmestimost' s yazykom S. 13.6 Obshirnyj interfejs Kogda obsuzhdalis' abstraktnye tipy ($$13.3) i uzlovye klassy ($$13.4), bylo podcherknuto, chto vse funkcii bazovogo klassa realizuyutsya v samom bazovom ili v proizvodnom klasse. No sushchestvuet i drugoj sposob postroeniya klassov. Rassmotrim, naprimer, spiski, massivy, associativnye massivy, derev'ya i t.d. Estestvenno zhelanie dlya vseh etih tipov, chasto nazyvaemyh kontejnerami, sozdat' obobshchayushchij ih klass, kotoryj mozhno ispol'zovat' v kachestve interfejsa s lyubym iz perechislennyh tipov. Ochevidno, chto pol'zovatel' ne dolzhen znat' detali, kasayushchiesya konkretnogo kontejnera. No zadacha opredeleniya interfejsa dlya obobshchennogo kontejnera netrivial'na. Predpolozhim, chto takoj kontejner budet opredelen kak abstraktnyj tip, togda kakie operacii on dolzhen predostavlyat'? Mozhno predostavit' tol'ko te operacii, kotorye est' v kazhdom kontejnere, t.e. peresechenie mnozhestv operacij, no takoj interfejs budet slishkom uzkim. Na samom dele, vo mnogih, imeyushchih smysl sluchayah takoe peresechenie pusto. V kachestve al'ternativnogo resheniya mozhno predostavit' ob容dinenie vseh mnozhestv operacij i predusmotret' dinamicheskuyu oshibku, kogda v etom interfejse k ob容ktu primenyaetsya "nesushchestvuyushchaya" operaciya. Ob容dinenie interfejsov klassov, predstavlyayushchih mnozhestvo ponyatij, nazyvaetsya obshirnym interfejsom. Opishem "obshchij" kontejner ob容ktov tipa T: class container { public: struct Bad_operation { // klass osobyh situacij const char* p; Bad_operation(const char* pp) : p(pp) { } }; virtual void put(const T*) { throw Bad_operation("container::put"); } virtual T* get() { throw Bad_operation("container::get"); } virtual T*& operator[](int) { throw Bad_operation("container::[](int)"); } virtual T*& operator[](const char*) { throw Bad_operation("container::[](char*)"); } // ... }; Vse-taki sushchestvuet malo realizacij, gde udachno predstavleny kak indeksirovanie, tak i operacii tipa spisochnyh, i, vozmozhno, ne stoit sovmeshchat' ih v odnom klasse. Otmetim takoe razlichie: dlya garantii proverki na etape translyacii v abstraktnom tipe ispol'zuyutsya chistye virtual'nye funkcii, a dlya obnaruzheniya oshibok na etape vypolneniya ispol'zuyutsya funkcii obshirnogo interfejsa, zapuskayushchie osobye situacii. Mozhno sleduyushchim obrazom opisat' kontejner, realizovannyj kak prostoj spisok s odnostoronnej svyaz'yu: class slist_container : public container, private slist { public: void put(const T*); T* get(); T*& operator[](int) { throw Bad_operation("slist::[](int)"); } T*& operator[](const* char) { throw Bad_operation("slist::[](char*)"); } // ... }; CHtoby uprostit' obrabotku dinamicheskih oshibok dlya spiska vvedeny operacii indeksirovaniya. Mozhno bylo ne vvodit' eti nerealizovannye dlya spiska operacii i ogranichit'sya menee polnoj informaciej, kotoruyu predostavlyayut osobye situacii, zapushchennye v klasse container: class vector_container : public container, private vector { public: T*& operator[](int); T*& operator[](const char*); // ... }; Esli byt' ostorozhnym, to vse rabotaet normal'no: void f() { slist_container sc; vector_container vc; // ... } void user(container& c1, container& c2) { T* p1 = c1.get(); T* p2 = c2[3]; // nel'zya ispol'zovat' c2.get() ili c1[3] // ... } Vse zhe dlya izbezhaniya oshibok pri vypolnenii programmy chasto prihoditsya ispol'zovat' dinamicheskuyu informaciyu o tipe ($$13.5) ili osobye situacii ($$9). Privedem primer: void user2(container& c1, container& c2) /* obnaruzhenie oshibki prosto, vosstanovlenie - trudnaya zadacha */ { try { T* p1 = c1.get(); T* p2 = c2[3]; // ... } catch(container::Bad_operation& bad) { // Priehali! // A chto teper' delat'? } } ili drugoj primer: void user3(container& c1, container& c2) /* obnaruzhenie oshibki neprosto, a vosstanovlenie po prezhnemu trudnaya zadacha */ { slist* sl = ptr_cast(slist_container,&c1); vector* v = ptr_cast(vector_container, &c2); if (sl && v) { T* p1 = c1.get(); T* p2 = c2[3]; // ... } else { // Priehali! // A chto teper' delat'? } } Oba sposoba obnaruzheniya oshibki, pokazannye na etih primerah, privodyat k programme s "razdutym" kodom i nizkoj skorost'yu vypolneniya. Poetomu obychno prosto ignoriruyut vozmozhnye oshibki v nadezhde, chto pol'zovatel' na nih ne natolknetsya. No zadacha ot etogo ne uproshchaetsya, ved' polnoe testirovanie zatrudnitel'no i trebuet mnogih usilij . Poetomu, esli cel'yu yavlyaetsya programma s horoshimi harakteristikami, ili trebuyutsya vysokie garantii korrektnosti programmy, ili, voobshche, est' horoshaya al'ternativa, luchshe ne ispol'zovat' obshirnye interfejsy. Krome togo, ispol'zovanie obshirnogo interfejsa narushaet vzaimnoodnoznachnoe sootvetstvie mezhdu klassami i ponyatiyami, i togda nachinayut vvodit' novye proizvodnye klassy prosto dlya udobstva realizacii. 13.7 Karkas oblasti prilozheniya My perechislili vidy klassov, iz kotoryh mozhno sozdat' biblioteki, nacelennye na proektirovanie i povtornoe ispol'zovanie prikladnyh programm. Oni predostavlyayut opredelennye "stroitel'nye bloki" i ob座asnyayut kak iz nih stroit'. Razrabotchik prikladnogo obespecheniya sozdaet karkas, v kotoryj dolzhny vpisat'sya universal'nye stroitel'nye bloki. Zadacha proektirovaniya prikladnyh programm mozhet imet' inoe, bolee obyazyvayushchee reshenie: napisat' programmu, kotoraya sama budet sozdavat' obshchij karkas oblasti prilozheniya. Razrabotchik prikladnogo obespecheniya v kachestve stroitel'nyh blokov budet vstraivat' v etot karkas prikladnye programmy. Klassy, kotorye obrazuyut karkas oblasti prilozheniya, imeyut nastol'ko obshirnyj interfejs, chto ih trudno nazvat' tipami v obychnom smysle slova. Oni priblizhayutsya k tomu predelu, kogda stanovyatsya chisto prikladnymi klassami, no pri etom v nih fakticheski est' tol'ko opisaniya, a vse dejstviya zadayutsya funkciyami, napisannymi prikladnymi programmistami. Dlya primera rassmotrim fil'tr, t.e. programmu, kotoraya mozhet vypolnyat' sleduyushchie dejstviya: chitat' vhodnoj potok, proizvodit' nad nim nekotorye operacii, vydavat' vyhodnoj potok i opredelyat' konechnyj rezul'tat. Primitivnyj karkas dlya fil'tra budet sostoyat' iz opredeleniya mnozhestva operacij, kotorye dolzhen realizovat' prikladnoj programmist: class filter { public: class Retry { public: virtual const char* message() { return 0; } }; virtual void start() { } virtual int retry() { return 2; } virtual int read() = 0; virtual void write() { } virtual void compute() { } virtual int result() = 0; }; Nuzhnye dlya proizvodnyh klassov funkcii opisany kak chistye virtual'nye, ostal'nye funkcii prosto pustye. Karkas soderzhit osnovnoj cikl obrabotki i zachatochnye sredstva obrabotki oshibok: int main_loop(filter* p) { for (;;) { try { p->start(); while (p->read()) { p->compute(); p->write(); } return p->result(); } catch (filter::Retry& m) { cout << m.message() << '\n'; int i = p->retry(); if (i) return i; } catch (...) { cout << "Fatal filter error\n"; return 1; } } } Teper' prikladnuyu programmu mozhno napisat' tak: class myfilter : public filter { istream& is; ostream& os; char c; int nchar; public: int read() { is.get(c); return is.good(); } void compute() { nchar++; }; int result() { os << nchar << "characters read\n"; return 0; } myfilter(istream& ii, ostream& oo) : is(ii), os(oo), nchar(0) { } }; i vyzyvat' ee sleduyushchim obrazom: int main() { myfilter f(cin,cout); return main_loop(&f); } Nastoyashchij karkas, chtoby rasschityvat' na primenenie v real'nyh zadachah, dolzhen sozdavat' bolee razvitye struktury i predostavlyat' bol'she poleznyh funkcij, chem v nashem prostom primere. Kak pravilo, karkas obrazuet derevo uzlovyh klassov. Prikladnoj programmist postavlyaet tol'ko klassy, sluzhashchie list'yami v etom mnogourovnevom dereve, blagodarya chemu dostigaetsya obshchnost' mezhdu razlichnymi prikladnymi programmami i uproshchaetsya povtornoe ispol'zovanie poleznyh funkcij, predostavlyaemyh karkasom. Sozdaniyu karkasa mogut sposobstvovat' biblioteki, v kotoryh opredelyayutsya nekotorye poleznye klassy, naprimer, takie kak scrollbar ($$12.2.5) i dialog_box ($$13.4). Posle opredeleniya svoih prikladnyh klassov programmist mozhet ispol'zovat' eti klassy. 13.8 Interfejsnye klassy Pro odin iz samyh vazhnyh vidov klassov obychno zabyvayut - eto "skromnye" interfejsnye klassy. Takoj klass ne vypolnyaet kakoj-to bol'shoj raboty, ved' inache, ego ne nazyvali by interfejsnym. Zadacha interfejsnom klassa prisposobit' nekotoruyu poleznuyu funkciyu k opredelennomu kontekstu. Dostoinstvo interfejsnyh klassov v tom, chto oni pozvolyayut sovmestno ispol'zovat' poleznuyu funkciyu, ne zagonyaya ee v zhestkie ramki. Dejstvitel'no, nevozmozhno rasschityvat', chto funkciya smozhet sama po sebe odinakovo horosho udovletvorit' samye raznye zaprosy. Interfejsnyj klass v chistom vide dazhe ne trebuet generacii koda. Vspomnim opisanie shablona tipa Splist iz $$8.3.2: template<class T> class Splist : private Slist<void*> { public: void insert(T* p) { Slist<void*>::insert(p); } void append(T* p) { Slist<void*>::append(p); } T* get() { return (T*) Slist<void*>::get(); } }; Klass Splist preobrazuet spisok nenadezhnyh obobshchennyh ukazatelej tipa void* v bolee udobnoe semejstvo nadezhnyh klassov, predstavlyayushchih spiski. CHtoby primenenie interfejsnyh klassov ne bylo slishkom nakladno, nuzhno ispol'zovat' funkcii-podstanovki. V primerah, podobnyh privedennomu, gde zadacha funkcij-podstanovok tol'ko podognat' tip, nakladnye rashody v pamyati i skorosti vypolneniya programmy ne voznikayut. Estestvenno, mozhno schitat' interfejsnym abstraktnyj bazovyj klass, kotoryj predstavlyaet abstraktnyj tip, realizuemyj konkretnymi tipami ($$13.3), takzhe kak i upravlyayushchie klassy iz razdela 13.9. No zdes' my rassmatrivaem klassy, u kotoryh net inyh naznachenij - tol'ko zadacha adaptacii interfejsa. Rassmotrim zadachu sliyaniya dvuh ierarhij klassov s pomoshch'yu mnozhestvennogo nasledovaniya. Kak byt' v sluchae kollizii imen, t.e. situacii, kogda v dvuh klassah ispol'zuyutsya virtual'nye funkcii s odnim imenem, proizvodyashchie sovershenno raznye operacii? Pust' est' videoigra pod nazvaniem "Dikij zapad", v kotoroj dialog s pol'zovatelem organizuetsya s pomoshch'yu okna obshchego vida (klass Window): class Window { // ... virtual void draw(); }; class Cowboy { // ... virtual void draw(); }; class CowboyWindow : public Cowboy, public Window { // ... }; V etoj igre klass CowboyWindow predstavlyaet dvizhenie kovboya na ekrane i upravlyaet vzaimodejstviem igroka s kovboem. Ochevidno, poyavitsya mnogo poleznyh funkcij, opredelennyh v klasse Window i Cowboy, poetomu predpochtitel'nee ispol'zovat' mnozhestvennoe nasledovanie, chem opisyvat' Window ili Cowboy kak chleny. Hotelos' by peredavat' etim funkciyam v kachestve parametra ob容kt tipa CowboyWindow, ne trebuya ot programmista ukazaniya kakih-to specifikacij ob容kta. Zdes' kak raz i voznikaet vopros, kakuyu funkcii vybrat' dlya CowboyWindow: Cowboy::draw() ili Window::draw(). V klasse CowboyWindow mozhet byt' tol'ko odna funkciya s imenem draw(), no poskol'ku poleznaya funkciya rabotaet s ob容ktami Cowboy ili Window i nichego ne znaet o CowboyWindow, v klasse CowboyWindow dolzhny podavlyat'sya (pereopredelyat'sya) i funkciya Cowboy::draw(), i funkciya Window_draw(). Podavlyat' obe funkcii s pomoshch'yu odnoj - draw() nepravil'no, poskol'ku, hotya ispol'zuetsya odno imya, vse zhe vse funkcii draw() razlichny i ne mogut pereopredelyat'sya odnoj. Nakonec, zhelatel'no, chtoby v klasse CowboyWindow nasleduemye funkcii Cowboy::draw() i Window::draw() imeli razlichnye odnoznachno zadannye imena. Dlya resheniya etoj zadachi nuzhno vvesti dopolnitel'nye klassy dlya Cowboy i Window. Vvoditsya dva novyh imeni dlya funkcij draw() i garantiruetsya, chto ih vyzov v klassah Cowboy i Window privedet k vyzovu funkcij s novymi imenami: class CCowboy : public Cowboy { virtual int cow_draw(int) = 0; void draw() { cow_draw(i); } // pereopredelenie Cowboy::draw }; class WWindow : public Window { virtual int win_draw() = 0; void draw() { win_draw(); } // pereopredelenie Window::draw }; Teper' s pomoshch'yu interfejsnyh klassov CCowboy i WWindow mozhno opredelit' klass CowboyWindow i sdelat' trebuemye pereopredeleniya funkcij cow_draw() i win_draw: class CowboyWindow : public CCowboy, public WWindow { // ... void cow_draw(); void win_draw(); }; Otmetim, chto v dejstvitel'nosti trudnost' voznikla lish' potomu, chto u obeih funkcij draw() odinakovyj tip parametrov. Esli by tipy parametrov razlichalis', to obychnye pravila razresheniya neodnoznachnosti pri peregruzke garantirovali by, chto trudnostej ne vozniknet, nesmotrya na nalichie razlichnyh funkcij s odnim imenem. Dlya kazhdogo sluchaya ispol'zovaniya interfejsnogo klassa mozhno predlozhit' takoe rasshirenie yazyka, chtoby trebuemaya adaptaciya prohodila bolee effektivno ili zadavalas' bolee elegantnym sposobom. No takie sluchai yavlyayutsya dostatochno redkimi, i net smysla chrezmerno peregruzhat' yazyk, predostavlyaya special'nye sredstva dlya kazhdogo otdel'nogo sluchaya. V chastnosti, sluchaj kollizii imen pri sliyanii ierarhij klassov dovol'no redki, osobenno esli sravnivat' s tem, naskol'ko chasto programmist sozdaet klassy. Takie sluchai mogut voznikat' pri sliyanii ierarhij klassov iz raznyh oblastej (kak v nashem primere: igry i operacionnye sistemy). Sliyanie takih raznorodnyh struktur klassov vsegda neprostaya zadacha, i razreshenie kollizii imen yavlyaetsya v nej daleko ne samoj trudnoj chast'yu. Zdes' voznikayut problemy iz-za raznyh strategij obrabotki oshibok, inicializacii, upravleniya pamyat'yu. Primer, svyazannyj s kolliziej imen, byl priveden potomu, chto predlozhennoe reshenie: vvedenie interfejsnyh klassov s funkciyami-perehodnikami, - imeet mnogo drugih primenenij. Naprimer, s ih pomoshch'yu mozhno menyat' ne tol'ko imena, no i tipy parametrov i vozvrashchaemyh znachenij, vstavlyat' opredelennye dinamicheskie proverki i t.d. Funkcii-perehodniki CCowboy::draw() i WWindow_draw yavlyayutsya virtual'nymi, i prostaya optimizaciya s pomoshch'yu podstanovki nevozmozhna. Odnako, est' vozmozhnost', chto translyator raspoznaet takie funkcii i udalit ih iz cepochki vyzovov. Interfejsnye funkcii sluzhat dlya prisposobleniya interfejsa k zaprosam pol'zovatelya. Blagodarya im v interfejse sobirayutsya operacii, razbrosannye po vsej programme. Obratimsya k klassu vector iz $$1.4. Dlya takih vektorov, kak i dlya massivov, indeks otschityvaetsya ot nulya. Esli pol'zovatel' hochet rabotat' s diapazonom indeksov, otlichnym ot diapazona 0..size-1, nuzhno sdelat' sootvetstvuyushchie prisposobleniya, naprimer, takie: void f() { vector v(10); // diapazon [0:9] // kak budto v v diapazone [1:10]: for (int i = 1; i<=10; i++) { v[i-1] = ... // ne zabyt' pereschitat' indeks } // ... } Luchshee reshenie daet klass vec c proizvol'nymi granicami indeksa: class vec : public vector { int lb; public: vec(int low, int high) : vector(high-low+1) { lb=low; } int& operator[](int i) { return vector::operator[](i-lb); } int low() { return lb; } int high() { return lb+size() - 1; } }; Klass vec mozhno ispol'zovat' bez dopolnitel'nyh operacij, neobhodimyh v pervom primere: void g() { vec v(1,10); // diapazon [1:10] for (int i = 1; i<=10; i++) { v[i] = ... } // ... } Ochevidno, variant s klassom vec naglyadnee i bezopasnee. Interfejsnye klassy imeyut i drugie vazhnye oblasti primeneniya, naprimer, interfejs mezhdu programmami na S++ i programmami na drugom yazyke ($$12.1.4) ili interfejs s osobymi bibliotekami S++. 13.9 Upravlyayushchie klassy Koncepciya abstraktnogo klassa daet effektivnoe sredstvo dlya razdeleniya interfejsa i ego realizacii. My primenyali etu koncepciyu i poluchali postoyannuyu svyaz' mezhdu interfejsom, zadannym abstraktnym tipom, i realizaciej, predstavlennoj konkretnym tipom. Tak, nevozmozhno pereklyuchit' abstraktnyj iterator s odnogo klassa-istochnika na drugoj, naprimer, esli ischerpano mnozhestvo (klass set), nevozmozhno perejti na potoki. Dalee, poka my rabotaem s ob容ktami abstraktnogo tipa s pomoshch'yu ukazatelej ili ssylok, teryayutsya vse preimushchestva virtual'nyh funkcij. Programma pol'zovatelya nachinaet zaviset' ot konkretnyh klassov realizacii. Dejstvitel'no, ne znaya razmera ob容kta, dazhe pri abstraktnom tipe nel'zya razmestit' ob容kt v steke, peredat' kak parametr po znacheniyu ili razmestit' kak staticheskij. Esli rabota s ob容ktami organizovana cherez ukazateli ili ssylki, to zadacha raspredeleniya pamyati perekladyvaetsya na pol'zovatelya ($$13.10). Sushchestvuet i drugoe ogranichenie, svyazannoe s ispol'zovaniem abstraktnyh tipov. Ob容kt takogo klassa vsegda imeet opredelennyj razmer, no klassy, otrazhayushchie real'noe ponyatie, mogut trebovat' pamyat' raznyh razmerov. Est' rasprostranennyj priem preodoleniya etih trudnostej, a imenno, razbit' otdel'nyj ob容kt na dve chasti: upravlyayushchuyu, kotoraya opredelyaet interfejs ob容kta, i soderzhatel'nuyu, v kotoroj nahodyatsya vse ili bol'shaya chast' atributov ob容kta. Svyaz' mezhdu dvumya chastyami realizuetsya s pomoshch'yu ukazatelya v upravlyayushchej chasti na soderzhatel'nuyu chast'. Obychno v upravlyayushchej chasti krome ukazatelya est' i drugie dannye, no ih nemnogo. Sut' v tom, chto sostav upravlyayushchej chasti ne menyaetsya pri izmenenii soderzhatel'noj chasti, i ona nastol'ko mala, chto mozhno svobodno rabotat' s samimi ob容ktami, a ne s ukazatelyami ili ssylkami na nih. upravlyayushchaya chast' soderzhatel'naya chast' Prostym primerom upravlyayushchego klassa mozhet sluzhit' klass string iz $$7.6. V nem soderzhitsya interfejs, kontrol' dostupa i upravlenie pamyat'yu dlya soderzhatel'noj chasti. V etom primere upravlyayushchaya i soderzhatel'naya chasti predstavleny konkretnymi tipami, no chashche soderzhatel'naya chast' predstavlyaetsya abstraktnym klassom. Teper' vernemsya k abstraktnomu tipu set iz $$13.3. Kak mozhno opredelit' upravlyayushchij klass dlya etogo tipa, i kakie eto dast plyusy i minusy? Dlya dannogo klassa set mozhno opredelit' upravlyayushchij klass prosto peregruzkoj operacii ->: class set_handle { set* rep; public: set* operator->() { return rep; } set_handler(set* pp) : rep(pp) { } }; |to ne slishkom vliyaet na rabotu s mnozhestvami, prosto peredayutsya ob容kty tipa set_handle vmesto ob容ktov tipa set& ili set*, naprimer: void my(set_handle s) { for (T* p = s->first(); p; p = s->next()) { // ... } // ... } void your(set_handle s) { for (T* p = s->first(); p; p = s->next()) { // ... } // ... } void user() { set_handle sl(new slist_set); set_handle v(new vector_set v(100)); my(sl); your(v); my(v); your(sl); } Esli klassy set i set_handle razrabatyvalis' sovmestno,legko realizovat' podschet chisla sozdavaemyh mnozhestv: class set { friend class set_handle; protected: int handle_count; public: virtual void insert(T*) = 0; virtual void remove(T*) = 0; virtual int is_member(T*) = 0; virtual T* first() = 0; virtual T* next() = 0; set() : handle_count(0) { } }; CHtoby podschitat' chislo ob容ktov dannogo tipa set, v upravlyayushchem klasse nuzhno uvelichivat' ili umen'shat' znachenie schetchika set_handle: class set_handle { set* rep; public: set* operator->() { return rep; } set_handle(set* pp) : rep(pp) { pp->handle_count++; } set_handle(const set_handle& r) : rep(r.rep) { rep->handle_count++; } set_handle& operator=(const set_handle& r) { rep->handle_count++; if (--rep->handle_count == 0) delete rep; rep = r.rep; return *this; } ~set_handle() { if (--rep->handle_count == 0) delete rep; } }; Esli vse obrashcheniya k klassu set obyazatel'no idut cherez set_handle, pol'zovatel' mozhet ne bespokoit'sya o raspredelenii pamyati pod ob容kty tipa set. Na praktike inogda prihoditsya izvlekat' ukazatel' na soderzhatel'nuyu chast' iz upravlyayushchego klassa i pol'zovat'sya neposredstvenno im. Mozhno, naprimer, peredat' takoj ukazatel' funkcii, kotoraya nichego ne znaet ob upravlyayushchem klasse. Esli funkciya ne unichtozhaet ob容kt, na kotoryj ona poluchila ukazatel', i esli ona ne sohranyaet ukazatel' dlya dal'nejshego ispol'zovaniya posle vozvrata, nikakih oshibok byt' ne dolzhno. Mozhet okazat'sya poleznym pereklyuchenie upravlyayushchego klassa na druguyu soderzhatel'nuyu chast': class set_handle { set* rep; public: // ... set* get_rep() { return rep; } void bind(set* pp) { pp->handle_count++; if (--rep->handle_count == 0) delete rep; rep = pp; } }; Sozdanie novyh proizvodnyh ot set_handle klassov obychno ne imeet osobogo smysla, poskol'ku eto - konkretnyj tip bez virtual'nyh funkcij. Drugoe delo - postroit' upravlyayushchij klass dlya semejstva klassov, opredelyaemyh odnim bazovym. Poleznym priemom budet sozdanie proizvodnyh ot takogo upravlyayushchego klassa. |tot priem mozhno primenyat' kak dlya uzlovyh klassov, tak i dlya abstraktnyh tipov. Estestvenno zadavat' upravlyayushchij klass kak shablon tipa: template<class T> class handle { T* rep; public: T* operator->() { return rep; } // ... }; No pri takom podhode trebuetsya vzaimodejstvie mezhdu upravlyayushchim i "upravlyaemym" klassami. Esli upravlyayushchij i upravlyaemye klassy razrabatyvayutsya sovmestno, naprimer, v processe sozdaniya biblioteki, to eto mozhet byt' dopustimo. Odnako, sushchestvuyut i drugie resheniya ($$13.10). Za schet peregruzki operacii -> upravlyayushchij klass poluchaet vozmozhnost' kontrolya i vypolneniya kakih-to operacij pri kazhdom obrashchenii k ob容ktu. Naprimer, mozhno vesti podschet chastoty ispol'zovaniya ob容ktov cherez upravlyayushchij klass: template<class T> class Xhandle { T* rep; int count; public: T* operator->() { count++; return rep; } // ... }; Nuzhna bolee slozhnaya tehnika, esli trebuetsya vypolnyat' operacii kak pered, tak i posle obrashcheniya k ob容ktu. Naprimer, mozhet potrebovat'sya mnozhestvo s blokirovkoj pri vypolnenii operacij dobavleniya k mnozhestvu i udaleniya iz nego. Zdes', po suti, v upravlyayushchem klasse prihoditsya dublirovat' interfejs s ob容ktami soderzhatel'noj chasti: class set_controller { set* rep; // ... public: lock(); unlock(); virtual void insert(T* p) { lock(); rep->insert(p); unlock(); } virtual void remove(T* p) { lock(); rep->remove(p); unlock(); } virtual int is_member(T* p) { return rep->is_member(p); } virtual T* first() { return rep->first(); } virtual T* next() { return rep->next(); } // ... }; Pisat' funkcii-perehodniki dlya vsego interfejsa utomitel'no (a znachit mogut poyavlyat'sya oshibki), no ne trudno i eto ne uhudshaet harakteristik programmy. Zametim, chto ne vse funkcii iz set sleduet blokirovat'. Kak pokazyvaet opyt avtora, tipichnyj sluchaj, kogda operacii do i posle obrashcheniya k ob容ktu nado vypolnyat' ne dlya vseh, a tol'ko dlya nekotoryh funkcij-chlenov. Blokirovka vseh operacij, kak eto delaetsya v monitorah nekotoryh operacionnyh sistem, yavlyaetsya izbytochnoj i mozhet sushchestvenno uhudshit' parallel'nyj rezhim vypolneniya. Pereopredeliv vse funkcii interfejsa v upravlyayushchem klasse, my poluchili po sravneniyu s priemom peregruz