Vvedenie | |
43 | 1.3.1. |ffektivnost' i struktura |
73 | 2.5.5. Virtual'nye funkcii |
79 | 2.7.2. Obobshchennye algoritmy |
128 | 5.1.1. Nol' |
192 | 7.4. Peregruzhennye imena funkcij |
199 | 7.6. Neukazannoe kolichestvo argumentov |
202 | 7.7. Ukazatel' na funkciyu |
296 | 10.4.6.2. CHleny-konstanty |
297 | 10.4.7. Massivy |
316 | 11.3.1. Operatory-chleny i ne-chleny |
328 | 11.5.1. Poisk druzej |
333 | 11.7.1. YAvnye konstruktory |
337 | 11.9. Vyzov funkcii |
344 | 11.12. Klass String |
351 | 12.2. Proizvodnye klassy |
361 | 12.2.6. Virtual'nye funkcii |
382 | 13.2.3. Parametry shablonov |
399 | 13.6.2. CHleny-shablony |
419 | 14.4.1. Ispol'zovanie konstruktorov i destruktorov |
421 | 14.4.2. auto_ptr |
422 | 14.4.4. Isklyucheniya i operator new |
431 | 14.6.1. Proverka specifikacij isklyuchenij |
431 | 14.6.3. Otobrazhenie isklyuchenij |
460 | 15.3.2. Dostup k bazovym klassam |
461 | 15.3.2.1. Mnozhestvennoe nasledovanie i upravlenie dostupom |
475 | 15.5. Ukazateli na chleny |
477 | 15.6. Svobodnaya pamyat' |
478 | 15.6. Svobodnaya pamyat' |
479 | 15.6.1. Vydelenie pamyati pod massiv |
480 | 15.6.2. "Virtual'nye konstruktory" |
498 | 16.2.3. STL-kontejnery |
505 | 16.3.4. Konstruktory |
508 | 16.3.5. Operacii so stekom |
526 | 17.1.4.1. Sravneniya |
541 | 17.4.1.2. Iteratory i pary |
543 | 17.4.1.3. Indeksaciya |
555 | 17.5.3.3. Drugie operacii |
556 | 17.6. Opredelenie novogo kontejnera |
583 | 18.4.4.1. Svyazyvateli |
584 | 18.4.4.2. Adaptery funkcij-chlenov |
592 | 18.6. Algoritmy, modificiruyushchie posledovatel'nost' |
592 | 18.6.1. Kopirovanie |
622 | 19.2.5. Obratnye iteratory |
634 | 19.4.1. Standartnyj raspredelitel' pamyati |
637 | 19.4.2. Raspredeliteli pamyati, opredelyaemye pol'zovatelem |
641 | 19.4.4. Neinicializirovannaya pamyat' |
647 | 20.2.1. Osobennosti simvolov |
652 | 20.3.4. Konstruktory |
655 | 20.3.6. Prisvaivanie |
676 | 21.2.2. Vyvod vstroennyh tipov |
687 | 21.3.4. Vvod simvolov |
701 | 21.4.6.3. Manipulyatory, opredelyaemye pol'zovatelem |
711 | 21.6.2. Potoki vvoda i bufera |
773 | 23.4.3.1. |tap 1: vyyavlenie klassov |
879 | A.5. Vyrazheniya |
931 | B.13.2. Druz'ya |
935 | B.13.6. template kak kvalifikator |
Optimizaciya | |
Makrosy | |
Ishodnyj kod |
V processe chteniya (i mnogokratnogo) perechityvaniya C++ 3rd u menya voznikalo mnozhestvo voprosov, bol'shaya chast' kotoryh otpadala posle izucheniya sobstvenno standarta i prodolzhitel'nyh razdumij, a za nekotorymi prihodilos' obrashchat'sya neposredstvenno k avtoru. Hochetsya vyrazit' bezuslovnuyu blagodarnost' d-ru Straustrupu za ego otvety na vse moi, zasluzhivayushchie vnimaniya, voprosy i razreshenie privesti dannye otvety zdes'.
Kak chitat' etu knigu. Prezhde vsego, nuzhno prochitat' "YAzyk programmirovaniya C++" i tol'ko na etape vtorogo ili tret'ego perechityvaniya obrashchat'sya k moemu materialu, t.k. zdes' krome ispravleniya oshibok russkogo perevoda izlagayutsya i ves'ma netrivial'nye veshchi, kotorye vryad li budut interesny srednemu programmistu na C++. Moej cel'yu bylo uluchshit' perevod C++ 3rd, naskol'ko eto vozmozhno i prolit' svet na mnozhestvo interesnyh osobennostej C++. Krome togo, original'noe (anglijskoe) izdanie perezhilo dovol'no mnogo tirazhej, i kazhdyj tirazh soderzhal nekotorye ispravleniya, ya postaralsya privesti vse sushchestvennye ispravleniya zdes'.
Esli vy chto-to ne ponyali v russkom perevode, to pervym delom stoit zaglyanut' v original: Bjarne Stroustrup "The C++ Programming language", 3rd edition i/ili v standart C++ (ISO/IEC 14882 Programming languages - C++, First edition, 1998-09-01). K slovu skazat', kak i lyuboj drugoj trud sravnimogo ob®ema i slozhnosti, standart C++ takzhe soderzhit oshibki. Dlya togo, chtoby byt' v kurse poslednih izmenenij standarta, budet poleznym prosmatrivat' C++ Standard Core Issues List i C++ Standard Library Issues List na ego official'noj stranice.
Takzhe ne pomeshaet oznakomit'sya s klassicheskoj STL, vedushchej nachalo neposredstvenno ot Aleksa Stepanova. I, glavnoe, ne zabud'te zaglyanut' k samomu B'ernu Straustrupu.
Kstati, esli vy eshche ne chitali "The C programming Language" by Brian W. Kernighan and Dennis M. Ritchie, 2e izdanie, to ya vam sovetuyu nepremenno eto sdelat' -- Klassika!
S uvazheniem, Sergej Derevyago.
new
, delete
, type_id
, dynamic_cast
, throw
i bloka try
, otdel'nye vyrazheniya i instrukcii C++ ne trebuyut podderzhki vo vremya vypolneniya.
Hotelos' by otmetit', chto est' eshche neskol'ko ochen' vazhnyh mest, gde my imeem neozhidannuyu i poroj ves'ma sushchestvennuyu "podderzhku vremeni vypolneniya". |to konstruktory/destruktory (slozhnyh) ob®ektov, kod sozdaniya/unichtozheniya massivov ob®ektov, prolog/epilog sozdayushchih ob®ekty funkcij i, otchasti, vyzovy virtual'nyh funkcij.
Dlya demonstracii dannoj pechal'noj osobennosti rassmotrim sleduyushchuyu programmu (zamechu, chto v ishodnom kode tekst programmy, kak pravilo, raznesen po neskol'kim fajlam dlya predotvrashcheniya agressivnogo vybrasyvaniya "mertvogo koda" kachestvennymi optimizatorami):
#include <stdio.h> #include <stdlib.h> #include <time.h> struct A { A(); ~A(); }; void ACon(); void ADes(); void f1() { A a; } void f2() { ACon(); ADes(); } long Var, Count; A::A() { Var++; } A::~A() { Var++; } void ACon() { Var++; } void ADes() { Var++; } int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1(); c2=clock(); printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2(); c2=clock(); printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } }V nej funkcii
f1()
i f2()
delayut odno i to zhe, tol'ko pervaya neyavno, s pomoshch'yu konstruktora i destruktora klassa A
, a vtoraya s pomoshch'yu yavnogo vyzova ACon()
i ADes()
.
Dlya raboty programma trebuet odnogo parametra -- skol'ko millionov raz vyzyvat' testovye funkcii. Vyberite znachenie, pozvolyayushchee f1()
rabotat' neskol'ko sekund i posmotrite na rezul'tat dlya f2()
.
Pri ispol'zovanii kachestvennogo optimizatora nikakoj raznicy byt' ne dolzhno; tem ne menee, na nekotoryh platformah ona opredelenno est' i poroj dostigaet 10 raz!
A chto zhe inline
? Davajte vnesem ochevidnye izmeneniya:
struct A { A() { Var++; } ~A() { Var++; } }; void f1() { A a; } void f2() { Var++; Var++; }Teper' raznicy vo vremeni raboty
f1()
i f2()
ne byt' dolzhno. K neschast'yu, na bol'shinstve kompilyatorov ona vse zhe prisutstvuet.
CHto zhe proishodit? Nablyudaemyj nami effekt nazyvaetsya abstraction penalty, t.e. obratnaya storona abstrakcii ili nalagaemoe na nas nekachestvennymi kompilyatorami nakazanie za ispol'zovanie (ob®ektno-orientirovannyh) abstrakcij.
Davajte posmotrim kak abstraction penalty proyavlyaetsya v nashem sluchae.
CHto zhe iz sebya predstavlyaet
void f1() { A a; }ekvivalentnoe
void f1() // psevdokod { A::A(); A::~A(); }I chem ono otlichaetsya ot prostogo vyzova dvuh funkcij:
void f2() { ACon(); ADes(); }V dannom sluchae -- nichem! No, davajte rassmotrim pohozhij primer:
void f1() { A a; f(); } void f2() { ACon(); f(); ADes(); }Kak vy dumaete, ekvivalentny li dannye funkcii? Pravil'nyj otvet -- net, t.k.
f1()
predstavlyaet soboj
void f1() // psevdokod { A::A(); try { f(); } catch (...) { A::~A(); throw; } A::~A(); }T.e. esli konstruktor uspeshno zavershil svoyu rabotu, to yazykom garantiruetsya, chto obyazatel'no budet vyzvan destruktor. T.e. tam, gde sozdayutsya nekotorye ob®ekty, kompilyator special'no vstavlyaet bloki obrabotki isklyuchenij dlya garantii vyzova sootvetstvuyushchih destruktorov. A nakladnye rashody v original'noj
f1()
chashche vsego budut vyzvany prisutstviem nenuzhnyh v dannom sluchae blokov obrabotki isklyuchenij (fakticheski, prisutstviem "utyazhelennyh" prologov/epilogov):
void f1() // psevdokod { A::A(); try { // pusto } catch (...) { A::~A(); throw; } A::~A(); }Delo v tom, chto kompilyator obyazan korrektno obrabatyvat' vse vozmozhnye sluchai, poetomu dlya uproshcheniya kompilyatora ego razrabotchiki chasto ne prinimayut vo vnimanie "chastnye sluchai", v kotoryh mozhno ne generirovat' nenuzhnyj kod. Uvy, podobnogo roda uproshcheniya kompilyatora ochen' ploho skazyvayutsya na proizvoditel'nosti intensivno ispol'zuyushchego sredstva abstrakcii i
inline
funkcii koda. Horoshim primerom podobnogo roda koda yavlyaetsya STL, ch'e ispol'zovanie, pri nalichii plohogo optimizatora, vyzyvaet chrezmernye nakladnye rashody.
Poeksperimentirujte so svoim kompilyatorom dlya opredeleniya ego abstraction penalty -- garantirovanno prigoditsya pri optimizacii "uzkih mest".
vtbl
dlya kazhdogo takogo klassa.
Na samom dele pervoe utverzhdenie neverno, t.e. ob®ekt poluchennyj v rezul'tate mnozhestvennogo nasledovaniya ot polimorfnyh klassov budet soderzhat' neskol'ko "unasledovannyh" ukazatelej na vtbl
.
Rassmotrim sleduyushchij primer. Pust' u nas est' polimorfnyj (t.e. soderzhashchij virtual'nye funkcii) klass B1
:
struct B1 { // ya napisal struct chtoby ne vozit'sya s pravami dostupa int a1; int b1; virtual ~B1() { } };I pust' imeyushchayasya u nas realizaciya razmeshchaet
vptr
(ukazatel' na tablicu virtual'nyh funkcij klassa) pered ob®yavlennymi nami chlenami. Togda dannye ob®ekta klassa B1
budut raspolozheny v pamyati sleduyushchim obrazom:
vptr_1 // ukazatel' na vtbl klassa B1 a1 // ob®yavlennye nami chleny b1Esli teper' ob®yavit' analogichnyj klass
B2
i proizvodnyj klass D
struct D: B1, B2 { virtual ~D() { } };to ego dannye budut raspolozheny sleduyushchim obrazom:
vptr_d1 // ukazatel' na vtbl klassa D, dlya B1 zdes' byl vptr_1 a1 // unasledovannye ot B1 chleny b1 vptr_d2 // ukazatel' na vtbl klassa D, dlya B2 zdes' byl vptr_2 a2 // unasledovannye ot B2 chleny b2Pochemu zdes' dva
vptr
? Potomu, chto byla provedena optimizaciya, inache ih bylo by tri.
YA, konechno, ponyal, chto vy imeli vvidu: "Pochemu ne odin"? Ne odin, potomu chto my imeem vozmozhnost' preobrazovyvat' ukazatel' na proizvodnyj klass v ukazatel' na lyuboj iz bazovyh klassov. Pri etom, poluchennyj ukazatel' dolzhen ukazyvat' na korrektnyj ob®ekt bazovogo klassa. T.e. esli ya napishu:
D d; B2* ptr=&d;to v nashem primere
ptr
ukazhet v tochnosti na vptr_d2
. A sobstvennym vptr
klassa D
budet yavlyat'sya vptr_d1
. Znacheniya etih ukazatelej, voobshche govorya, razlichny. Pochemu? Potomu chto u B1
i B2
v vtbl
po odnomu i tomu zhe indeksu mogut byt' raspolozheny raznye virtual'nye funkcii, a D
dolzhen imet' vozmozhnost' ih pravil'no zamestit'. T.o. vtbl
klassa D
sostoit iz neskol'kih chastej: chast' dlya B1
, chast' dlya B2
i chast' dlya sobstvennyh nuzhd.
Podvodya itog, mozhno skazat', chto esli my ispol'zuem mnozhestvennoe nasledovanie ot bol'shogo chisla polimorfnyh klassov, to nakladnye rashody po pamyati mogut byt' dostatochno sushchestvennymi.
Sudya po vsemu, ot etih rashodov mozhno otkazat'sya, realizovav vyzov virtual'noj funkcii special'nym obrazom, a imenno: kazhdyj raz vychislyaya polozhenie vptr
otnositel'no this
i pereschityvaya indeks vyzyvaemoj virtual'noj funkcii v vtbl
. Odnako eto sprovociruet sushchestvennye rashody vremeni vypolneniya, chto nepriemlemo.
I raz uzh tak mnogo slov bylo skazano pro effektivnost', davajte real'no izmerim otnositel'nuyu stoimost' vyzova virtual'noj funkcii.
#include <stdio.h> #include <stdlib.h> #include <time.h> struct B { void f(); virtual void vf(); }; struct D : B { void vf(); // zameshchaem B::vf }; void f1(B* ptr) { ptr->f(); } void f2(B* ptr) { ptr->vf(); } long Var, Count; void B::f() { Var++; } void B::vf() { } void D::vf() { Var++; } int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; D d; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1(&d); c2=clock(); printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2(&d); c2=clock(); printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } }V zavisimosti ot kompilyatora i platformy, nakladnye rashody na vyzov virtual'noj funkcii sostavili ot 10% do 2.5 raz. T.o. mozhno utverzhdat', chto "virtual'nost'" nebol'shih funkcij mozhet obojtis' sravnitel'no dorogo.
I slovo "nebol'shih" zdes' ne sluchajno, t.k. uzhe dazhe test s funkciej Akkermana (otlichno podhodyashchej dlya vyyavleniya otnositel'noj stoimosti vyzova)
#include <stdio.h> #include <stdlib.h> #include <time.h> struct B { int ackf(int x, int y); virtual int vackf(int x, int y); }; struct D : B { int vackf(int x, int y); // zameshchaem B::vackf }; void f1(B* ptr) { ptr->ackf(3, 5); // 42438 vyzovov! } void f2(B* ptr) { ptr->vackf(3, 5); // 42438 vyzovov! } int B::ackf(int x, int y) { if (x==0) return y+1; else if (y==0) return ackf(x-1, 1); else return ackf(x-1, ackf(x, y-1)); } int B::vackf(int x, int y) { return 0; } int D::vackf(int x, int y) { if (x==0) return y+1; else if (y==0) return vackf(x-1, 1); else return vackf(x-1, vackf(x, y-1)); } long Count; int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; D d; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f1(&d); c2=clock(); printf("f1(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f2(&d); c2=clock(); printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }pokazyvaet zametno drugie rezul'taty, sushchestvenno umen'shaya otnositel'nuyu raznost' vremeni vypolneniya.
char vc1[200]; char vc2[500]; void f() { copy(&vc1[0],&vc1[200],&vc2[0]); }
Nu, esli k delu podojti formal'no, to zapisat' my tak ne mozhem. Vot chto govorit ob etom d-r Straustrup:
The issue is whether taking the address of one-past-the-last element of an array is conforming C and C++. I could make the example clearly conforming by a simple rewrite:copy(vc1,vc1+200,vc2);However, I don't want to introduce addition to pointers at this point of the book. It is a surprise to most experienced C and C++ programmers that&vc1[200]
isn't completely equivalent tovc1+200
. In fact, it was a surprise to the C committee also and I expect it to be fixed in the upcoming revision of the standard. (also resolved for C9x - bs 10/13/98).Sut' voprosa v tom, razresheno li v C i C++ vzyatie adresa elementa, sleduyushchego za poslednim elementom massiva. YA mog sdelat' primer ochevidno korrektnym prostoj zamenoj:
copy(vc1,vc1+200,vc2);Odnako, ya ne hotel vvodit' slozhenie s ukazatelem v etoj chasti knigi. Dazhe dlya samyh opytnyh programmistov na C i C++ bol'shim syurprizom yavlyaetsya tot fakt, chto&vc1[200]
ne polnost'yu ekvivalentnovc1+200
. Fakticheski, eto okazalos' neozhidannost'yu i dlya C komiteta, i ya ozhidayu, chto eto nedorazumenie budet ustraneno v sleduyushchih redakciyah standarta.
Tak v chem zhe narushaetsya ekvivalentnost'? Po standartu C++ my imeem sleduyushchie ekvivalentnye preobrazovaniya:
&vc1[200] -> &(*((vc1)+(200))) -> &*(vc1+200)Dejstvitel'no li ravenstvo
&*(vc1+200) == vc1+200
neverno?
It is false in C89 and C++, but not in K&R C or C9x. The C89 standard simply said that&*(vc1+200)
means dereferencevc1+200
(which is an error) and then take the address of the result, and the C++ standard copiled the C89 wording. K&R C and C9x say that&*
cancels out so that&*(vc1+200) == vc2+200
.|to neverno v S89 i C++, no ne v K&R C ili S9h. Standart S89 govorit, chto
&*(vc1+200)
oznachaet razymenovanievc1+200
(chto yavlyaetsya oshibkoj) i zatem vzyatie adresa rezul'tata. I standart C++ prosto vzyal etu formulirovku iz S89. Odnako K&R C i S9h ustanavlivayut, chto&*
vzaimno unichtozhayutsya, t.e.&*(vc1+200) == vc1+200
.
Speshu vas uspokoit', chto na praktike v vyrazhenii &*(vc1+200)
nekorrektnoe razymenovanie *(vc1+200)
prakticheski nikogda ne proizojdet, t.k. rezul'tatom vsego vyrazheniya yavlyaetsya adres i ni odin ser'eznyj kompilyator ne stanet vybirat' znachenie po nekotoromu adresu (operaciya razymenovaniya) chtoby potom poluchit' tot zhe samyj adres s pomoshch'yu operacii &
.
NULL
, vospol'zujtes'
const int NULL=0;
Sut' dannogo soveta v tom, chto soglasno opredeleniyu yazyka ne sushchestvuet konteksta, v kotorom (opredelennoe v zagolovochnom fajle) znachenie NULL
bylo by korrektnym, v to vremya kak prosto 0
-- net.
Ishodya iz togo zhe opredeleniya, peredacha NULL
v funkcii s peremennym kolichestvom parametrov vmesto korrektnogo vyrazheniya vida static_cast<SomeType*>(0)
zapreshchena.
Bezuslovno, vse eto pravil'no, no na praktike NULL
v funkcii s peremennym kolichestvom parametrov vse zhe peredayut. Naprimer, tak:
#include <stdio.h> #include <stdarg.h> #include <stdlib.h> void error(int stat ...) { va_list ap; va_start(ap, stat); while (const char* sarg=va_arg(ap, const char *)) printf("%s", sarg); va_end(ap); exit(stat); } int main() { error(1, "Sluchilos' ", "strashnoe", NULL); // vnimanie, oshibka! // vmesto NULL nuzhno ispol'zovat' // static_cast<const char *>(0) }Imenno dlya podderzhki podobnogo roda praktiki (nekorrektnoj, no shiroko rasprostranennoj) realizaciyam razresheno opredelyat'
NULL
kak 0L
(a ne prosto 0
) na arhitekturah, gde sizeof(void*)==sizeof(long)>sizeof(int)
.
Privedennyj v knige punkt [2] nuzhno zamenit' na:
bool
v int
, char
v int
, short
v int;
§ B.6.1), float
v double
.
struct A { private: void f(int); public: void f(...); }; void g() { A a; a.f(1); // oshibka: vybiraetsya A::f(int), ispol'zovanie // kotoroj v g() zapreshcheno }Otsutstvie dannogo pravila porodilo by tonkie oshibki, kogda vybor podhodyashchej funkcii zavisel by ot mesta vyzova: v funkcii-chlene ili v obychnoj funkcii.
va_start()
, neobhodimo osushchestvit' vyzov va_end()
. Prichina sostoit v tom, chto va_start()
mozhet modificirovat' stek takim obrazom, chto stanet nevozmozhen normal'nyj vyhod iz funkcii.
Vvidu chego voznikayut sovershenno nezametnye podvodnye kamni.
Obshcheizvestno, chto obrabotka isklyucheniya predpolagaet raskrutku steka. Sledovatel'no, esli v moment vozbuzhdeniya isklyucheniya funkciya izmenila stek, to u vas garantirovanno budut nepriyatnosti.
Takim obrazom, do vyzova va_end()
sleduet vozderzhivat'sya ot potencial'no vyzyvayushchih isklyucheniya operacij. Special'no dobavlyu, chto vvod/vyvod C++ mozhet generirovat' isklyucheniya, t.e. "naivnaya" tehnika vyvoda v std::cout
do vyzova va_end()
chrevata nepriyatnostyami.
cmp3
v kachestve argumenta ssort()
narushilo by garantiyu togo, chto ssort()
vyzovetsya s argumentami mytype*
.
Zdes' imeet mesto dosadnaya opechatka, sovershenno iskazhayushchaya smysl predlozheniya. Sleduet chitat' tak: Prichina v tom, chto razreshenie ispol'zovaniya cmp3
v kachestve argumenta ssort()
narushilo by garantiyu togo, chto cmp3()
vyzovetsya s argumentami mytype*
.
Vrode by vse horosho, no pochemu tol'ko integral'nogo tipa? V chem prichina podobnoj diskriminacii? D-r Straustrup pishet po etomu povodu sleduyushchee:
The reason for "discriminating against" floating points in constant expressions is that the precision of floating point traditionally varied radically between processors. In principle, constant expressions should be evaluated on the target processor if you are cross compiling.Prichina podobnoj "diskriminacii" plavayushchej arifmetiki v konstantnyh vyrazheniyah v tom, chto obychno tochnost' podobnyh operacij na raznyh processorah sushchestvenno otlichaetsya. V principe, esli vy osushchestvlyaete kross-kompilyaciyu, to takie konstantnye vyrazheniya dolzhny vychislyat'sya na celevom processore.
T.e. v processe kross-kompilyacii na processore drugoj arhitektury budet krajne problematichno absolyutno tochno vychislit' konstantnoe vyrazhenie, kotoroe moglo by byt' ispol'zovano v kachestve literala (a ne adresa yachejki pamyati) v mashinnyh komandah celevogo processora.
Sudya po vsemu, za predelami zadach kross-kompilyacii (kotorye, k slovu skazat', vstrechayutsya ne tak uzh i chasto) nikakih problem s opredeleniem necelochislennyh konstant ne voznikaet, t.k. nekotorye kompilyatory vpolne dopuskayut kod vida
class Curious { static const float c5=7.0; };v kachestve (neperenosimogo) rasshireniya yazyka.
K schast'yu, eto ogranichenie mozhno sravnitel'no legko obojti. Naprimer, posredstvom vvedeniya lokal'nogo klassa:
#include <stdio.h> struct A { // ishodnyj klass int a; A(int a_) : a(a_) { printf("%d\n",a); } }; void f() { static int vals[]={2, 0, 0, 4}; static int curr=0; struct A_local : public A { // vspomogatel'nyj lokal'nyj A_local() : A(vals[curr++]) { } }; A_local arr[4]; // i dalee ispol'zuem kak A arr[4]; } int main() { f(); }T.k. lokal'nye klassy i ih ispol'zovanie ostalis' za ramkami knigi, dalee privoditsya sootvetstvuyushchij razdel standarta:
9.8 Ob®yavleniya lokal'nyh klassov [class.local]
int x; void f() { static int s; int x; extern int g(); struct local { int g() { return x; } // oshibka, auto x int h() { return s; } // OK int k() { return ::x; } // OK int l() { return g(); } // OK }; // ... } local* p = 0; // oshibka: net local v tekushchem kontekste
Y
mozhet byt' ob®yavlen vnutri lokal'nogo klassa X
i opredelen vnutri opredeleniya klassa X
ili zhe za ego predelami, no v tom zhe kontekste (scope), chto i klass X
. Vlozhennyj klass lokal'nogo klassa sam yavlyaetsya lokal'nym.
complex r1=x+y+z; // r1=operator+(x,operator+(y,z))
Na samom dele dannoe vyrazhenie budet prointerpretirovano tak:
complex r1=x+y+z; // r1=operator+(operator+(x,y),z)Potomu chto operaciya slozheniya levoassociativna:
(x+y)+z
.
// net f() v dannoj oblasti vidimosti class X { friend void f(); // bespolezno friend void h(const X&); // mozhet byt' najdena po argumentam }; void g(const X& x) { f(); // net f() v dannoj oblasti vidimosti h(x); // h() -- drug X }On vzyat iz spiska avtorskih ispravlenij k 8-mu tirazhu i pokazyvaet, chto esli
f
ne bylo v oblasti vidimosti, to ob®yavlenie funkcii-druga f()
vnutri klassa X
ne vnosit imya f
v oblast' vidimosti, tak chto popytka vyzova f()
iz g()
yavlyaetsya oshibkoj.
String s1='a'; // oshibka: net yavnogo preobrazovaniya char v String String s2(10); // pravil'no: stroka dlya hraneniya 10 simvolovmozhet pokazat'sya ochen' tonkoj...
No ona nesomnenno est'. I delo tut vot v chem.
Zapis'
X a=b;vsegda oznachaet sozdanie ob®ekta
a
klassa X
posredstvom kopirovaniya znacheniya nekotorogo drugogo ob®ekta klassa X
. Zdes' mozhet byt' dva varianta:
b
uzhe yavlyaetsya ob®ektom klassa X
. V etom sluchae my poluchim neposredstvennyj vyzov konstruktora kopirovaniya:
X a(b);
b
ob®ektom klassa X
ne yavlyaetsya. V etom sluchae dolzhen byt' sozdan vremennyj ob®ekt klassa X
, ch'e znachenie budet zatem skopirovano:
X a(X(b));Imenno etot vremennyj ob®ekt i ne mozhet byt' sozdan v sluchae explicit-konstruktora, chto privodit k oshibke kompilyacii.
12.8 Kopirovanie ob®ektov klassov [class.copy]
return
vyrazhenie yavlyaetsya imenem lokal'nogo ob®ekta, tip kotorogo (ignoriruya cv-kvalifikatory) sovpadaet s tipom vozvrata, realizacii razresheno ne sozdavat' vremennyj ob®ekt dlya hraneniya vozvrashchaemogo znacheniya, dazhe esli konstruktor kopirovaniya ili destruktor imeyut pobochnye effekty. V etih sluchayah ob®ekt budet unichtozhen pozdnee, chem byli by unichtozheny original'nyj ob®ekt i ego kopiya, esli by dannaya optimizaciya ne ispol'zovalas'.
#include <stdio.h> #include <string.h> struct A { static const int nsize=10; char n[nsize]; A(char cn) { n[0]=cn; n[1]=0; printf("%5s.A::A()\n", n); } A(const A& a) { if (strlen(a.n)<=nsize-2) { n[0]='?'; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::A(const A& %s)\n", n, a.n); } ~A() { printf("%5s.A::~A()\n", n); } A& operator=(const A& a) { if (strlen(a.n)<=nsize-2) { n[0]='='; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::operator=(const A& %s)\n", n, a.n); return *this; } }; A f1(A a) { printf("A f1(A %s)\n", a.n); return a; } A f2() { printf("A f2()\n"); A b('b'); return b; } A f3() { printf("A f3()\n"); return A('c'); } int main() { { A a('a'); A b='b'; A c(A('c')); A d=A('d'); } printf("----------\n"); { A a('a'); A b=f1(a); printf("b eto %s\n", b.n); } printf("----------\n"); { A a=f2(); printf("a eto %s\n", a.n); } printf("----------\n"); { A a=f3(); printf("a eto %s\n", a.n); } }Prezhde vsego, v
main()
raznymi sposobami sozdayutsya ob®ekty a
, b
, c
i d
. V normal'noj realizacii vy poluchite sleduyushchij vyvod:
a.A::A() b.A::A() c.A::A() d.A::A() d.A::~A() c.A::~A() b.A::~A() a.A::~A()Tam zhe, gde razrabotchiki kompilyatora shalturili, poyavyatsya nenuzhnye vremennye ob®ekty, naprimer:
... c.A::A() ?c.A::A(const A& c) c.A::~A() d.A::A() d.A::~A() ?c.A::~A() ...T.e.
A c(A('c'))
prevratilos' v A tmp('c'), c(tmp)
. Dalee, vyzov f1()
demonstriruet neyavnye vyzovy konstruktorov kopirovaniya vo vsej krase:
a.A::A() ?a.A::A(const A& a) A f1(A ?a) ??a.A::A(const A& ?a) ?a.A::~A() b eto ??a ??a.A::~A() a.A::~A()Na osnovanii
a
sozdaetsya vremennyj ob®ekt ?a
, i peredaetsya f1()
kachestve argumenta. Dalee, vnutri f1()
na osnovanii ?a
sozdaetsya drugoj vremennyj ob®ekt -- ??a
, on nuzhen dlya vozvrata znacheniya. I vot tut-to i proishodit isklyuchenie novogo vremennogo ob®ekta: b eto ??a
, t.e. lokal'naya peremennaya main()
b
-- eto ta samaya, sozdannaya v f1()
peremennaya ??a
, a ne ee kopiya (special'no dlya somnevayushchihsya: bud' eto ne tak, my by uvideli b eto ???a
).
Polnost'yu soglasen -- vse eto dejstvitel'no ochen' zaputano, no razobrat'sya vse zhe stoit. Dlya bolee yavnoj demonstracii isklyucheniya vremennoj peremennoj ya napisal f2()
i f3()
:
A f2() b.A::A() ?b.A::A(const A& b) b.A::~A() a eto ?b ?b.A::~A() ---------- A f3() c.A::A() a eto c c.A::~A()V
f3()
ono proishodit, a v f2()
-- net! Kak govoritsya, vse delo v volshebnyh puzyr'kah.
Drugogo ob®yasneniya net, t.k. vremennaya peremennaya mogla byla isklyuchena v oboih sluchayah (oh uzh mne eti pisateli kompilyatorov!).
A sejchas rassmotrim bolee interesnyj sluchaj -- peregruzku operatorov. Vnesem v nash klass sootvetstvuyushchie izmeneniya:
#include <stdio.h> #include <string.h> struct A { static const int nsize=10; static int tmpcount; int val; char n[nsize]; A(int val_) : val(val_) // dlya sozdaniya vremennyh ob®ektov { sprintf(n, "_%d", ++tmpcount); printf("%5s.A::A(int %d)\n", n, val); } A(char cn, int val_) : val(val_) { n[0]=cn; n[1]=0; printf("%5s.A::A(char, int %d)\n", n, val); } A(const A& a) : val(a.val) { if (strlen(a.n)<=nsize-2) { n[0]='?'; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::A(const A& %s)\n", n, a.n); } ~A() { printf("%5s.A::~A()\n", n); } A& operator=(const A& a) { val=a.val; if (strlen(a.n)<=nsize-2) { n[0]='='; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::operator=(const A& %s)\n", n, a.n); return *this; } friend A operator+(const A& a1, const A& a2) { printf("operator+(const A& %s, const A& %s)\n", a1.n, a2.n); return A(a1.val+a2.val); } }; int A::tmpcount; int main() { A a('a', 1), b('b', 2), c('c', 3); A d=a+b+c; printf("d eto %s\n", d.n); printf("d.val=%d\n", d.val); }Posle zapuska vy dolzhny poluchit' sleduyushchie rezul'taty:
a.A::A(char,int 1) b.A::A(char,int 2) c.A::A(char,int 3) operator+(const A& a,const A& b) _1.A::A(int 3) operator+(const A& _1,const A& c) _2.A::A(int 6) _1.A::~A() d eto _2 d.val=6 _2.A::~A() c.A::~A() b.A::~A() a.A::~A()Vse dovol'no naglyadno, tak chto ob®yasneniya izlishni. A dlya demonstracii raboty operatora prisvaivaniya poprobujte
A d('d',0); d=a+b+c;V dannom sluchae budet zadejstvovano na odnu vremennuyu peremennuyu bol'she:
a.A::A(char,int 1) b.A::A(char,int 2) c.A::A(char,int 3) d.A::A(char,int 0) operator+(const A& a,const A& b) _1.A::A(int 3) operator+(const A& _1,const A& c) _2.A::A(int 6) =_2.A::operator=(const A& _2) _2.A::~A() _1.A::~A() d eto =_2 d.val=6 =_2.A::~A() c.A::~A() b.A::~A() a.A::~A()
operator()()
ob®ekta Add(z)
.
Ispol'zovanie shablonov i smysl ih parametrov mozhet stat' dlya vas sovershenno neponyatnym, esli raz i navsegda ne uyasnit' odnu prostuyu veshch': pri vyzove funkcii-shablona vy peredaete ob®ekty, no kriticheski vazhnoj dlya instanciirovaniya shablonov informaciej yavlyayutsya tipy peredannyh ob®ektov. Sejchas ya proillyustriruyu dannuyu ideyu na privedennom v knige primere.
Rassmotrim, naprimer, opredelenie funkcii-shablona for_each()
template <class InputIter, class Function> Function for_each(InputIter first, InputIter last, Function f) { for ( ; first != last; ++first) f(*first); return f; }Dannoe opredelenie ya vzyal neposredstvenno iz sgi STL (predvaritel'no ubrav simvoly podcherkivaniya dlya uluchsheniya chitaemosti). Esli sravnit' ego s privedennym v knige, to srazu brosaetsya v glaza ispravlenie tipa vozvrashchaemogo znacheniya (po standartu dolzhen byt' argument-funkciya) i otkaz ot ispol'zovaniya potencial'no menee effektivnogo postinkrementa iteratora.
Kogda my vyzyvaem for_each()
c argumentom Add(z)
,
for_each(ll.begin(), ll.end(), Add(z));to
Function
-- eto Add
, t.e. tip, a ne ob®ekt Add(z)
. I po opredeleniyu for_each()
kompilyatorom budet sgenerirovan sleduyushchij kod:
Add for_each(InputIter first, InputIter last, Add f) { for ( ; first != last; ++first) f.operator()(*first); return f; }T.o. v moment vyzova
for_each()
budet sozdan vremennyj ob®ekt Add(z)
, kotoryj zatem i budet peredan v kachestve argumenta. Posle chego, vnutri for_each()
dlya kopii etogo ob®ekta budet vyzyvat'sya Add::operator()(complex&)
. Konechno, tip InputIter
takzhe budet zamenen tipom sootvetstvuyushchego iteratora, no v dannyj moment eto nas ne interesuet.
Na chto zhe ya hochu obratit' vashe vnimanie? YA hochu otmetit', chto shablon -- eto ne makros v kotoryj peredaetsya chto-to, k chemu mozhno pripisat' skobki s sootvetstvuyushchimi argumentami. Esli by shablon byl makrosom, neposredstvenno prinimayushchim peredannyj ob®ekt, to my by poluchili
Add for_each(...) { for (...) Add(z).operator()(*first); return f; }chto, v principe, tozhe korrektno, tol'ko krajne neeffektivno: pri kazhdom prohode cikla sozdaetsya vremennyj ob®ekt, k kotoromu zatem primenyaetsya operaciya vyzova funkcii.
String
s.operator[](1)
oznachaet Cref(s,1)
.
A vot zdes' hotelos' by popodrobnee. Pochemu v odnom klasse my mozhem ob®yavit' const
i ne const
funkcii-chleny? Kak osushchestvlyaetsya vybor peregruzhennoj funkcii?
Rassmotrim sleduyushchee ob®yavlenie:
struct X { void f(int); void f(int) const; }; void h() { const X cx; cx.f(1); X x; x.f(2); }Vvidu togo, chto funkciya-chlen vsegda imeet skrytyj parametr
this
, kompilyator vosprinimaet dannoe ob®yavlenie kak
// psevdokod struct X { void f( X *const this); void f(const X *const this); }; void h() { const X cx; X::f(&cx,1); X x; X::f(&x,2); }i vybor peregruzhennoj funkcii osushchestvlyaetsya po obychnym pravilam. V obshchem, nikakoj mistiki.
Vmeste s tem, dannaya terminologiya sovershenno estestvenna v teoretiko-mnozhestvennom smysle. A imenno: kazhdyj ob®ekt proizvodnogo klassa yavlyaetsya ob®ektom bazovogo klassa, a obratnoe, voobshche govorya, neverno. T.o. bazovyj klass shire, poetomu on i superklass. Putanica voznikaet iz-za togo, chto bol'she sam klass, a ne ego ob®ekty, kotorye vvidu bol'shej obshchnosti klassa dolzhny imet' men'she osobennostej (chlenov).
|to, voobshche govorya, neverno. Pri primenenii mnozhestvennogo nasledovaniya "prosto kosvennogo vyzova" okazyvaetsya nedostatochno. Rassmotrim sleduyushchuyu programmu:
#include <stdio.h> struct B1 { int b1; // nepustaya virtual ~B1() { } }; struct B2 { int b2; // nepustaya virtual void vfun() { } }; struct D : B1, B2 { // mnozhestvennoe nasledovanie ot nepustyh klassov virtual void vfun() { printf("D::vfun(): this=%p\n", this); } }; int main() { D d; D* dptr=&d; printf("dptr\t%p\n", dptr); dptr->vfun(); B2* b2ptr=&d; printf("b2ptr\t%p\n", b2ptr); b2ptr->vfun(); }Na svoej mashine ya poluchil sleduyushchie rezul'taty:
dptr 0x283fee8 D::vfun(): this=0x283fee8 b2ptr 0x283feec D::vfun(): this=0x283fee8T.e. pri vyzove cherez ukazatel' na proizvodnyj klass
dptr
, vnutri D::vfun()
my poluchim this=0x283fee8
. No nesmotrya na to, chto posle preobrazovaniya ishodnogo ukazatelya v ukazatel' na (vtoroj) bazovyj klass b2ptr
, ego znachenie (ochevidno) izmenilos', vnutri D::vfun()
my vse ravno vidim ishodnoe znachenie, chto polnost'yu sootvetstvuet ozhidaniyam D::vfun()
otnositel'no tipa i znacheniya svoego this
.
CHto zhe vse eto oznachaet? A oznachaet eto to, chto esli by vyzov virtual'noj funkcii
struct D : B1, B2 { virtual void vfun(D *const this) // psevdokod { // ... } };cherez ukazatel'
ptr->vfun()
vsegda svodilsya by k vyzovu (*vtbl[index_of_vfun])(ptr)
, to v nashej programme my by poluchili b2ptr==0x283feec==this!=0x283fee8
.
Vopros nomer dva: kak oni eto delayut? Sut' problemy v tom, chto odna i ta zhe zameshchennaya virtual'naya funkciya (D::vfun()
v nashem sluchae) mozhet byt' vyzvana kak cherez ukazatel' na proizvodnyj klass (ptr==0x283fee8
) tak i cherez ukazatel' na odin iz bazovyh klassov (ptr==0x283feec
), ch'i znacheniya ne sovpadayut, v to vremya kak peredannoe znachenie this
dolzhno byt' odnim i tem zhe (this==0x283fee8
) v oboih sluchayah.
K schast'yu, vtbl
soderzhit raznye zapisi dlya kazhdogo iz variantov vyzova, tak chto reshenie, ochevidno, est'. Na praktike, chashche vsego, ispol'zuetsya odin iz sleduyushchih sposobov:
vtbl
dobavlyaetsya dopolnitel'naya kolonka -- vdelta
. Togda v processe vyzova virtual'noj funkcii krome adresa funkcii iz vtbl
izvlekaetsya i del'ta, ch'e znachenie dobavlyaetsya k ptr
:
addr=vtbl[index].vaddr; // izvlekaem adres funkcii vfun delta=vtbl[index].vdelta; // izvlekaem del'tu, zavisyashchuyu ot sposoba vyzova vfun (*addr)(ptr+delta); // vyzyvaem vfunSushchestvennym nedostatkom dannogo sposoba yavlyaetsya zametnoe uvelichenie razmerov
vtbl
i znachitel'nye nakladnye rashody vremeni vypolneniya: delo v tom, chto absolyutnoe bol'shinstvo vyzovov virtual'nyh funkcij ne trebuet korrekcii znacheniya ptr
, tak chto sootvetstvuyushchie im znacheniya vdelta
budut nulevymi. Dostoinstvom -- vozmozhnost' vyzova virtual'noj funkcii iz ANSI C koda, chto vazhno dlya C++ -> C translyatorov.
ptr
(esli eto voobshche nuzhno):
vfun_entry_0: // ... // sobstvenno kod vfun // ... return; vfun_entry_1: ptr+=delta_1; // korrektiruem znachenie ptr goto vfun_entry_0; // i perehodim k telu vfunV etom sluchae
vtbl
soderzhit tol'ko adresa sootvetstvuyushchih tochek vhoda i nikakih naprasnyh vychislenij ne trebuetsya. Specificheskim nedostatkom dannogo sposoba yavlyaetsya nevozmozhnost' ego realizacii sredstvami ANSI C.
Potomu chto strokovyj literal -- eto ob®ekt s vnutrennej komponovkoj (internal linkage).
M-da... Opredelenno, ne samoe udachnoe mesto russkogo perevoda. Tem bolee, chto v originale vse predel'no prosto i ponyatno:
Curiously enough, a template constructor is never used to generate a copy constructor, so without the explicitly declared copy constructor, a default copy constructor would have been generated.Kak ni stranno, konstruktor-shablon nikogda ne ispol'zuetsya dlya generacii konstruktora kopirovaniya, t.e. bez yavno opredelennogo konstruktora kopirovaniya budet sgenerirovan konstruktor kopirovaniya po umolchaniyu.
Dalee hochu otmetit', chto postoyanno vstrechayushchuyusya v perevode frazu "konstruktor shablona" sleduet ponimat' kak "konstruktor-shablon".
Esli vy reshili, chto tem samym dolzhna povysit'sya proizvoditel'nost', vvidu togo, chto v tele funkcii otsutstvuyut bloki try/catch
, to dolzhen vas ogorchit' -- oni budut avtomaticheski sgenerirovany kompilyatorom dlya korrektnoj obrabotki raskrutki steka. No vse-taki, kakaya versiya vydeleniya resursov obespechivaet bol'shuyu proizvoditel'nost'? Davajte protestiruem sleduyushchij kod:
#include <stdio.h> #include <stdlib.h> #include <time.h> void ResourceAcquire(); void ResourceRelease(); void Work(); struct RAII { RAII() { ResourceAcquire(); } ~RAII() { ResourceRelease(); } }; void f1() { ResourceAcquire(); try { Work(); } catch (...) { ResourceRelease(); throw; } ResourceRelease(); } void f2() { RAII raii; Work(); } long Var, Count; void ResourceAcquire() { Var++; } void ResourceRelease() { Var--; } void Work() { Var+=2; } int main(int argc, char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1, c2; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1(); c2=clock(); printf("f1(): %ld mln calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2(); c2=clock(); printf("f2(): %ld mln calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }Kak vydumaete, kakaya funkciya rabotaet bystree? A vot i net! V zavisimosti ot kompilyatora bystree rabotaet to
f1()
, to f2()
, a inogda oni rabotayut sovershenno odinakovo iz-za polnoj identichnosti sgenerirovannogo kompilyatorom koda. Vse zavisit ot ispol'zuemyh principov obrabotki isklyuchenij i kachestva optimizatora.
Kak zhe rabotayut isklyucheniya? Esli vkratce, to v raznyh realizaciyah isklyucheniya rabotayut po-raznomu. I vsegda chrezvychajno netrivial'no! Osobenno mnogo slozhnostej voznikaet s OS, ispol'zuyushchimi tak nazyvaemyj Structured Exception Handling i/ili podderzhivayushchimi mnogopotochnost' (multithreading). Fakticheski, s privychnymi nam sovremennymi OS...
Na tekushchij moment v Internet mozhno najti dostatochnoe kolichestvo materiala po realizacii exception handling (EH) v C++ i ne tol'ko, privodit' zdes' kotoryj ne imeet osobogo smysla. Tem ne menee, vliyanie EH na proizvoditel'nost' C++ programm zasluzhivaet otdel'nogo obsuzhdeniya.
Uvy, no staraniyami nedobrosovestnyh "preuvelichitelej dostoinstv" v massy poshel mif o tom, chto obrabotku isklyuchenij mozhno realizovat' voobshche bez nakladnyh rashodov. Na samom dele eto ne tak, t.k. dazhe samyj sovershennyj metod realizacii EH, otslezhivayushchij sozdannye (i, sledovatel'no, podlezhashchie unichtozheniyu) na dannyj moment (pod)ob®ekty po znacheniyu schetchika komand (naprimer, registr (E)IP processorov Intel-arhitektury) ne srabatyvaet v sluchae sozdaniya massivov.
No bolee nadezhnym (i, kstati, ne zavisyashchim ot sposoba realizacii EH) oproverzheniem ishodnoj posylki yavlyaetsya tot fakt, chto EH dobavlyaet dopolnitel'nye dugi v Control Flow Graph, t.e. v graf potokov upravleniya, chto ne mozhet ne skazat'sya na vozmozhnostyah optimizaci.
Tem ne menee, nakladnye rashody na EH v luchshih realizaciyah ne prevyshayut 5%, chto s prakticheskoj tochki zreniya pochti ekvivalentno polnomu otsutstviyu rashodov.
No eto v luchshih realizaciyah! O tom, chto proishodit v realizaciyah "obychnyh" luchshe ne upominat' -- kak govorit geroj izvestnogo anekdota: "Gadkoe zrelishche"...
auto_ptr
<memory>
auto_ptr
ob®yavlen sleduyushchim obrazom...
Vvidu togo, chto posle vyhoda pervyh (anglijskih) tirazhej standart preterpel nekotorye izmeneniya v chasti auto_ptr
, koncovku dannogo razdela sleduet zamenit' sleduyushchim tekstom (on vzyat iz spiska avtorskih ispravlenij k 4 tirazhu).
Dlya dostizheniya dannoj semantiki vladeniya (takzhe nazyvaemoj semantikoj razrushayushchego kopirovaniya (destructive copy semantics)), semantika kopirovaniya shablona auto_ptr
radikal'no otlichaetsya ot semantiki kopirovaniya obychnyh ukazatelej: kogda odin auto_ptr
kopiruetsya ili prisvaivaetsya drugomu, ishodnyj auto_ptr
ochishchaetsya (ekvivalentno prisvaivaniyu 0
ukazatelyu). T.k. kopirovanie auto_ptr
privodit k ego izmeneniyu, to const auto_ptr
ne mozhet byt' skopirovan.
SHablon auto_ptr
opredelen v <memory>
sleduyushchim obrazom:
template<class X> class std::auto_ptr { // vspomogatel'nyj klass template <class Y> struct auto_ptr_ref { /* ... */ }; X* ptr; public: typedef X element_type; explicit auto_ptr(X* p =0) throw() { ptr=p; } ~auto_ptr() throw() { delete ptr; } // obratite vnimanie: konstruktory kopirovaniya i operatory // prisvaivaniya imeyut nekonstantnye argumenty // skopirovat', potom a.ptr=0 auto_ptr(auto_ptr& a) throw(); // skopirovat', potom a.ptr=0 template<class Y> auto_ptr(auto_ptr<Y>& a) throw(); // skopirovat', potom a.ptr=0 auto_ptr& operator=(auto_ptr& a) throw(); // skopirovat', potom a.ptr=0 template<class Y> auto_ptr& operator=(auto_ptr<Y>& a) throw(); X& operator*() const throw() { return *ptr; } X* operator->() const throw() { return ptr; } // vernut' ukazatel' X* get() const throw() { return ptr; } // peredat' vladenie X* release() throw() { X* t = ptr; ptr=0; return t; } void reset(X* p =0) throw() { if (p!=ptr) { delete ptr; ptr=p; } } // skopirovat' iz auto_ptr_ref auto_ptr(auto_ptr_ref<X>) throw(); // skopirovat' v auto_ptr_ref template<class Y> operator auto_ptr_ref<Y>() throw(); // razrushayushchee kopirovanie iz auto_ptr template<class Y> operator auto_ptr<Y>() throw(); };Naznachenie
auto_ptr_ref
-- obespechit' semantiku razrushayushchego kopirovaniya, vvidu chego kopirovanie konstantnogo auto_ptr
stanovitsya nevozmozhnym. Konstruktor-shablon i operator prisvaivaniya-shablon obespechivayut vozmozhnost' neyavnogo prebrazovaniya auto_ptr<D>
v auto_ptr<B>
esli D*
mozhet byt' preobrazovan v B*
, naprimer:
void g(Circle* pc) { auto_ptr<Circle> p2 = pc; // sejchas p2 otvechaet za udalenie auto_ptr<Circle> p3 = p2; // sejchas p3 otvechaet za udalenie, // a p2 uzhe net p2->m = 7; // oshibka programmista: p2.get()==0 Shape* ps = p3.get(); // izvlechenie ukazatelya auto_ptr<Shape> aps = p3; // peredacha prav sobstvennosti i // preobrazovanie tipa auto_ptr<Circle> p4 = pc; // oshibka: teper' p4 takzhe otvechaet za udalenie }|ffekt ot ispol'zovaniya neskol'kih
auto_ptr
dlya odnogo i togo zhe ob®ekta neopredelen; v bol'shinstve sluchaev ob®ekt budet unichtozhen dvazhdy, chto privedet k razrushitel'nym rezul'tatam.
Sleduet otmetit', chto semantika razrushayushchego kopirovaniya ne udovletvoryaet trebovaniyam k elementam standartnyh kontejnerov ili standartnyh algoritmov, takih kak sort()
. Naprimer:
// opasno: ispol'zovanie auto_ptr v kontejnere void h(vector<auto_ptr<Shape> >& v) { sort(v.begin(),v.end()); // ne delajte tak: elementy ne budut otsortirovany }Ponyatno, chto
auto_ptr
ne yavlyaetsya obychnym "umnym" ukazatelem, odnako on prekrasno spravlyaetsya s predostavlennoj emu rol'yu -- obespechivat' bezopasnuyu otnositel'no isklyuchenij rabotu s avtomaticheskimi ukazatelyami, i delat' eto bez sushchestvennyh nakladnyh rashodov.
new
T.k. privedennye v knige ob®yasneniya nemnogo tumanny, vot sootvetstvuyushchaya chast' standarta:
5.3.4. New [expr.new]
typedef
ne mozhet ee soderzhat'.
Srazu zhe voznikaet vopros: v chem prichina etogo neudobnogo ogranicheniya? D-r Straustrup pishet po etomu povodu sleduyushchee:
The reason is the exception spacification is not part of the type; it is a constraint that is checked on assignment and exforced at run time (rather than at compile time). Some people would like it to be part of the type, but it isn't. The reason is to avoid difficulties when updating large systems with parts from different sources. See "The Design and Evolution of C++" for details.Prichina v tom, chto specifikacii isklyuchenij ne yavlyayutsya chast'yu tipa; dannoe ogranichenie proveryaetsya pri prisvaivanii i prinuditel'no obespechivaetsya vo vremya vypolneniya (a ne vo vremya kompilyacii). Nekotorym lyudyam hotelos' by, chtoby specifikacii isklyuchenij byli chast'yu tipa, no eto ne tak. Prichina v tom, chto my hotim izbezhat' trudnostej, voznikayushchih pri vnesenii izmenenij v bol'shie sistemy, sostoyashchie iz otdel'nyh chastej poluchennyh iz raznyh istochnikov. Obratites' k knige "Dizajn i evolyuciya C++" za detalyami.
Po moemu mneniyu, specifikacii vozbuzhdaemyh isklyuchenij -- eto odna iz samyh neudachnyh chastej opredeleniya C++. Istoricheski, neadekvatnost' sushchestvuyushchego mehanizma specifikacii isklyuchenij obuslovlena otsutstviem real'nogo opyta sistematicheskogo primeneniya isklyuchenij v C++ (i voznikayushchih pri etom voprosov exception safety) na moment ih vvedeniya v opredelenie yazyka. K slovu skazat', o slozhnosti problemy govorit i tot fakt, chto v Java, poyavivshemsya zametno pozzhe C++, specifikacii vozbuzhdaemyh isklyuchenij tak zhe realizovany neudachno.
Imeyushchijsya na tekushchij moment opyt svidetel'stvuet o tom, chto kriticheski vazhnoj dlya napisaniya exception safe koda informaciej yavlyaetsya otvet na vopros: Mozhet li funkciya voobshche vozbuzhdat' isklyucheniya? |ta informaciya izvestna uzhe na etape kompilyacii i mozhet byt' proverena bez osobogo truda.
Tak, naprimer, mozhno vvesti klyuchevoe slovo nothrow
:
// klyuchevoe slovo nothrow otsutstvuet: // f() razresheno vozbuzhdat' lyubye isklyucheniya pryamo ili kosvenno void f() { // ... }
// f() zapreshcheno vozbuzhdat' lyubye isklyucheniya pryamo ili kosvenno, // proveryaetsya na etape kompilyacii void f() nothrow { // ... }
void f() { // zdes' mozhno vozbuzhdat' isklyucheniya pryamo ili kosvenno nothrow { // nothrow-blok // kod, nahodyashchijsya v dannom bloke nikakih isklyuchenij vozbuzhdat' // ne dolzhen, proveryaetsya na etape kompilyacii } // zdes' snova mozhno vozbuzhdat' isklyucheniya }
std::bad_exception
opisannym v dannom razdele obrazom. Vot chto ob etom pishet d-r Straustrup:
The standard doesn't support the mapping of exceptions as I describe it in 14.6.3. It specifies mapping tostd::bad_exception
for exceptions thrown explicitly within anunexpected()
function. This makesstd::bad_exception
an ordinary and rather pointless exception. The current wording does not agree with the intent of the proposer of the mechanism (Dmitry Lenkov of HP) and what he thought was voted in. I have raised the issue in the standards committee.Standart ne podderzhivaet otobrazhenie isklyuchenij v tom vide, kak eto bylo mnoj opisano v razdele 14.6.3. On specificiruet otobrazhenie v
std::bad_exception
tol'ko dlya isklyuchenij, yavno vozbuzhdennyh v funkciiunexpected()
. |to lishaetstd::bad_exception
pervonachal'nogo smysla, delaya ego obychnym i sravnitel'no bessmyslennym isklyucheniem. Tekushchaya formulirovka (standarta) ne sovpadaet s pervonachal'no predlozhennoj Dmitriem Lenkovym iz HP. YA vozbudil sootvetstvuyushchee issue v komitete po standartizacii.
Nu i raz uzh stol'ko slov bylo skazano pro formulirovku iz standarta, dumayu, chto stoit ee privesti:
15.5.2 Funkciya unexpected()
[except.unexpected]
void unexpected();srazu zhe posle zaversheniya raskrutki steka (stack unwinding).
unexpected()
ne mozhet vernut' upravlenie, no mozhet (pere)vozbudit' isklyuchenie. Esli ona vozbuzhdaet novoe isklyuchenie, kotoroe razresheno narushennoj do etogo specifikaciej isklyuchenij, to poisk podhodyashchego obrabotchika budet prodolzhen s tochki vyzova sgenerirovavshej neozhidannoe isklyuchenie funkcii. Esli zhe ona vozbudit nedozvolennoe isklyuchenie, to: Esli specifikaciya isklyuchenij ne soderzhit klass std::bad_exception
(18.6.2.1), to budet vyzvana terminate()
, inache (pere)vozbuzhdennoe isklyuchenie budet zameneno na opredelyaemyj realizaciej ob®ekt tipa std::bad_exception
i poisk sootvetstvuyushchego obrabotchika budet prodolzhen opisannym vyshe sposobom.
std::bad_exception
, to lyuboe neopisannoe isklyuchenie mozhet byt' zameneno na std::bad_exception
vnutri unexpected()
.
class XX : B { /* ... */ }; // B -- zakrytyj bazovyj klass class YY : B { /* ... */ }; // B -- otkrytaya bazovaya struktura
Na samom dele, v originale bylo tak:
class XX : B { /* ... */ }; // B -- zakrytaya baza struct YY : B { /* ... */ }; // B -- otkrytaya bazaT.e. vne zavisimosti ot togo, yavlyaetsya li baza
B
klassom ili strukturoj, prava dostupa k unasledovannym chlenam opredelyayutsya tipom naslednika: po umolchaniyu, klass zakryvaet dostup k svoim unasledovannym bazam, a struktura -- otkryvaet.
V principe, v etom net nichego neozhidannogo -- dostup po umolchaniyu k obychnym, ne unasledovannym, chlenam zadaetsya temi zhe pravilami.
Tut, konechno, imeet mesto dosadnaya opechatka, chto, kstati skazat', srazu vidno iz privedennogo primera. T.e. chitat' sleduet tak: ... esli on razreshen po nekotoromu iz vozmozhnyh putej.
|to utverzhdenie, voobshche govorya, neverno i ya vam sovetuyu nikogda tak ne postupat'. Sejchas pokazhu pochemu.
Prezhde vsego, stoit otmetit', chto v C++ vy ne smozhete pryamo vyvesti znachenie ukazatelya na chlen:
struct S { int i; void f(); }; void g() { cout<<&S::i; // oshibka: operator<< ne realizovan dlya tipa int S::* cout<<&S::f; // oshibka: operator<< ne realizovan dlya tipa void (S::*)() }|to dovol'no stranno. Andrew Koenig pishet po etomu povodu, chto delo ne v nedosmotre razrabotchikov biblioteki vvoda/vyvoda, a v tom, chto ne sushchestvuet perenosimogo sposoba dlya vyvoda chego-libo soderzhatel'nogo (kstati, ya okazalsya pervym, kto voobshche ob etom sprosil, tak chto problemu opredelenno nel'zya nazvat' zlobodnevnoj). Moe zhe mnenie sostoit v tom, chto kazhdaya iz realizacij vpolne sposobna najti sposob dlya vyvoda bolee-menee soderzhatel'noj informacii, t.k. v dannom sluchae dazhe neideal'noe reshenie -- eto gorazdo luchshe, chem voobshche nichego.
Poetomu dlya illyustracii vnutrennego predstavleniya ukazatelej na chleny ya napisal sleduyushchij primer:
#include <string.h> #include <stdio.h> struct S { int i1; int i2; void f1(); void f2(); virtual void vf1(); virtual void vf2(); }; const int SZ=sizeof(&S::f1); union { unsigned char c[SZ]; int i[SZ/sizeof(int)]; int S::* iptr; void (S::*fptr)(); } hack; void printVal(int s) { if (s%sizeof(int)) for (int i=0; i<s; i++) printf(" %02x", hack.c[i]); else for (int i=0; i<s/sizeof(int); i++) printf(" %0*x", sizeof(int)*2, hack.i[i]); printf("\n"); memset(&hack, 0, sizeof(hack)); } int main() { printf("sizeof(int)=%d sizeof(void*)=%d\n", sizeof(int), sizeof(void*)); hack.iptr=&S::i1; printf("sizeof(&S::i1 )=%2d value=", sizeof(&S::i1)); printVal(sizeof(&S::i1)); hack.iptr=&S::i2; printf("sizeof(&S::i2 )=%2d value=", sizeof(&S::i2)); printVal(sizeof(&S::i2)); hack.fptr=&S::f1; printf("sizeof(&S::f1 )=%2d value=", sizeof(&S::f1)); printVal(sizeof(&S::f1)); hack.fptr=&S::f2; printf("sizeof(&S::f2 )=%2d value=", sizeof(&S::f2)); printVal(sizeof(&S::f2)); hack.fptr=&S::vf1; printf("sizeof(&S::vf1)=%2d value=", sizeof(&S::vf1)); printVal(sizeof(&S::vf1)); hack.fptr=&S::vf2; printf("sizeof(&S::vf2)=%2d value=", sizeof(&S::vf2)); printVal(sizeof(&S::vf2)); } void S::f1() {} void S::f2() {} void S::vf1() {} void S::vf2() {}Sushchestvennymi dlya ponimaniya mestami zdes' yavlyayutsya ob®edinenie
hack
, ispol'zuemoe dlya preobrazovaniya znacheniya ukazatelej na chleny v posledovatel'nost' bajt (ili celyh), i funkciya printVal()
, pechatayushchaya dannye znacheniya.
YA zapuskal vysheprivedennyj primer na treh kompilyatorah, vot rezul'taty:
sizeof(int)=4 sizeof(void*)=4 sizeof(&S::i1 )= 8 value= 00000005 00000000 sizeof(&S::i2 )= 8 value= 00000009 00000000 sizeof(&S::f1 )=12 value= 004012e4 00000000 00000000 sizeof(&S::f2 )=12 value= 004012ec 00000000 00000000 sizeof(&S::vf1)=12 value= 004012d0 00000000 00000000 sizeof(&S::vf2)=12 value= 004012d8 00000000 00000000 sizeof(int)=4 sizeof(void*)=4 sizeof(&S::i1 )= 4 value= 00000001 sizeof(&S::i2 )= 4 value= 00000005 sizeof(&S::f1 )= 8 value= ffff0000 004014e4 sizeof(&S::f2 )= 8 value= ffff0000 004014f4 sizeof(&S::vf1)= 8 value= 00020000 00000008 sizeof(&S::vf2)= 8 value= 00030000 00000008 sizeof(int)=4 sizeof(void*)=4 sizeof(&S::i1 )= 4 value= 00000004 sizeof(&S::i2 )= 4 value= 00000008 sizeof(&S::f1 )= 4 value= 00401140 sizeof(&S::f2 )= 4 value= 00401140 sizeof(&S::vf1)= 4 value= 00401150 sizeof(&S::vf2)= 4 value= 00401160Prezhde vsego v glaza brosaetsya to, chto nesmotrya na odinakovyj razmer
int
i void*
, kazhdaya iz realizacij postaralas' otlichit'sya v vybore predstavleniya ukazatelej na chleny, osobenno pervaya. CHto zhe my mozhem skazat' eshche?
Ukazateli na funkcii-chleny vo vtorom kompilyatore realizovany neoptimal'no, t.k. inogda oni soderzhat ukazatel' na "obychnuyu" funkciyu (ffff0000 004014e4
), a inogda -- indeks virtual'noj funkcii (00020000 00000008
). V rezul'tate chego, vmesto togo, chtoby srazu proizvesti kosvennyj vyzov funkcii, kompilyator proveryaet starshuyu chast' pervogo int
, i esli tam stoit -1
(ffff
), to on imeet delo s obychnoj funkciej chlenom, inache -- s virtual'noj. Podobnogo roda proverki pri kazhdom vyzove funkcii-chlena cherez ukazatel' vyzyvayut nenuzhnye nakladnye rashody.
Vnimatel'nyj chitatel' dolzhen sprosit': "Horosho, pust' oni vsegda soderzhat obychnyj ukazatel' na funkciyu, no kak togda byt' s ukazatelyami na virtual'nye funkcii? Ved' my ne mozhem ispol'zovat' odin konkretnyj adres, tak kak virtual'nye funkcii prinyato zameshchat' v proizvodnyh klassah." Pravil'no, dorogoj chitatel'! No vyhod est', i on ocheviden: v etom sluchae kompilyator avtomaticheski generiruet promezhutochnuyu funkciyu-zaglushku.
Naprimer, sleduyushchij kod:
struct S { virtual void vf() { /* 1 */ } void f () { /* 2 */ } }; void g(void (S::*fptr)(), S* sptr) { (sptr->*fptr)(); } int main() { S s; g(S::vf, &s); g(S::f , &s); }prevrashchaetsya v psevdokod:
void S_vf(S *const this) { /* 1 */ } void S_f (S *const this) { /* 2 */ } void S_vf_stub(S *const this) { // virtual'nyj vyzov funkcii S::vf() (this->vptr[index_of_vf])(this); } void g(void (*fptr)(S *const), S* sptr) { fptr(sptr); } int main() { S s; g(S_vf_stub, &s); // obratite vnimanie: ne S_vf !!! g(S_f , &s); }A esli by v C++ prisutstvoval otdel'nyj tip "ukazatel' na virtual'nuyu funkciyu-chlen", on byl by predstavlen prostym indeksom virtual'noj funkcii, t.e. fakticheski prostym
size_t
, i generacii funkcij-zaglushek (so vsemi vytekayushchimi poteryami proizvoditel'nosti) bylo by mozhno izbezhat'. Bolee togo, ego, kak i ukazatel' na dannye-chlen, vsegda mozhno bylo by peredavat' v drugoe adresnoe prostranstvo.
p
ukazyvaet na s
bajtov pamyati, vydelennoj Employee::operator new()
Dannoe predpolozhenie ne vpolne korrektno: p
takzhe mozhet yavlyat'sya nulevym ukazatelem, i v etom sluchae opredelyaemyj pol'zovatelem operator delete()
dolzhen korretno sebya vesti, t.e. nichego ne delat'.
Zapomnite: opredelyaya operator delete()
, vy obyazany pravil'no obrabatyvat' udalenie nulevogo ukazatelya! T.o. kod dolzhen vyglyadet' sleduyushchim obrazom:
void Employee::operator delete(void* p, size_t s) { if (!p) return; // ignoriruem nulevoj ukazatel' // polagaem, chto p ukazyvaet na s bajtov pamyati, vydelennoj // Employee::operator new() i osvobozhdaem etu pamyat' // dlya dal'nejshego ispol'zovaniya }Interesno otmetit', chto standartom special'no ogovoreno, chto argument
p
funkcii
template <class T> void std::allocator::deallocate(pointer p, size_type n);ne mozhet byt' nulevym. Bez etogo zamechaniya ispol'zovanie funkcii
Pool::free
v razdele 19.4.2. "Raspredeliteli pamyati, opredelyaemye pol'zovatelem" bylo by nekorrektnym.
Imenno tak. T.e. esli vy ob®yavili destruktor nekotorogo klassa
A::~A() { // telo destruktora }to kompilyatorom (chashche vsego) budet sgenerirovan sleduyushchij kod
// psevdokod A::~A(A *const this, bool flag) { if (this) { // telo destruktora if (flag) delete(this, sizeof(A)); } }Vvidu chego funkciya
void f(Employee* ptr) { delete ptr; }prevratitsya v
// psevdokod void f(Employee* ptr) { Employee::~Employee(ptr, true); }i t.k. klass
Employee
imeet virtual'nyj destruktor, eto v konechnom itoge privedet k vyzovu sootvetstvuyushchego metoda.
new[]
svyazany ne vpolne ochevidnye veshchi. Ne mudrstvuya lukavo, privozhu perevod razdela 10.3 "Array Allocation" iz knigi "The Design and Evolution of C++" odnogo izvestnogo avtora:
Opredelennyj dlya klassa X
operator X::operator new()
ispol'zuetsya isklyuchitel'no dlya razmeshcheniya odinochnyh ob®ektov klassa X
(vklyuchaya ob®ekty proizvodnyh ot X
klassov, ne imeyushchih sobstvennogo raspredelitelya pamyati). Sledovatel'no
X* p = new X[10];ne vyzyvaet
X::operator new()
, t.k. X[10]
yavlyaetsya massivom, a ne ob®ektom klassa X
.
|to vyzyvalo mnogo zhalob, t.k. ya ne razreshil pol'zovatelyam kontrolirovat' razmeshchenie massivov tipa X
. Odnako ya byl nepreklonen, t.k. massiv elementov tipa X
-- eto ne ob®ekt tipa X
, i, sledovatel'no, raspredelitel' pamyati dlya X
ne mozhet byt' ispol'zovan. Esli by on ispol'zovalsya i dlya raspredeleniya massivov, to avtor X::operator new()
dolzhen byl by imet' delo kak s raspredeleniem pamyati pod ob®ekt, tak i pod massiv, chto sil'no uslozhnilo by bolee rasprostranennyj sluchaj. A esli raspredelenie pamyati pod massiv ne ochen' kritichno, to stoit li voobshche o nem bespokoit'sya? Tem bolee, chto vozmozhnost' upravleniya razmeshcheniem odnomernyh massivov, takih kak X[d]
ne yavlyaetsya dostatochnoj: chto, esli my zahotim razmestit' massiv X[d][d2]
?
Odnako, otsutstvie mehanizma, pozvolyayushchego kontrolirovat' razmeshchenie massivov vyzyvalo opredelennye slozhnosti v real'nyh programmah, i, v konce koncov, komitet po standartizacii predlozhil reshenie dannoj problemy. Naibolee kritichnym bylo to, chto ne bylo vozmozhnosti zapretit' pol'zovatelyam razmeshchat' massivy v svobodnoj pamyati, i dazhe sposoba kontrolirovat' podobnoe razmeshchenie. V sistemah, osnovannyh na logicheski raznyh shemah upravleniya razmeshcheniem ob®ektov eto vyzyvalo ser'eznye problemy, t.k. pol'zovateli naivno razmeshchali bol'shie dinamicheskie massivy v obychnoj pamyati. YA nedoocenil znachenie dannogo fakta.
Prinyatoe reshenie zaklyuchaetsya v prostom predostavlenii pary funkcij, special'no dlya razmeshcheniya/osvobozhdeniya massivov:
class X { // ... void* operator new(size_t sz); // raspredelenie ob®ektov void operator delete(void* p); void* operator new[](size_t sz); // raspredelenie massivov void operator delete[](void* p); };Raspredelitel' pamyati dlya massivov ispol'zuetsya dlya massivov lyuboj razmernosti. Kak i v sluchae drugih raspredelitelej, rabota
operator new[]
sostoit v predostavlenii zaproshennogo kolichestva bajt; emu ne nuzhno samomu bespokoit'sya o razmere ispol'zuemoj pamyati. V chastnosti, on ne dolzhen znat' o razmernosti massiva ili kolichestve ego elementov. Laura Yaker iz Mentor Graphics byla pervoj, kto predlozhil operatory dlya razmeshcheniya i osvobozhdeniya massivov.
Sleduet otmetit', chto eti "nekotorye oslableniya" ne yavlyayutsya prostoj formal'nost'yu. Rassmotrim sleduyushchij primer:
#include <stdio.h> struct B1 { int b1; // nepustaya virtual ~B1() { } }; struct B2 { int b2; // nepustaya virtual B2* vfun() { printf("B2::vfun()\n"); // etogo my ne dolzhny uvidet' return this; } }; struct D : B1, B2 { // mnozhestvennoe nasledovanie ot nepustyh klassov virtual D* vfun() { printf("D::vfun(): this=%p\n", this); return this; } }; int main() { D d; D* dptr=&d; printf("dptr\t%p\n", dptr); void* ptr1=dptr->vfun(); printf("ptr1\t%p\n", ptr1); B2* b2ptr=&d; printf("b2ptr\t%p\n", b2ptr); void* ptr2=b2ptr->vfun(); printf("ptr2\t%p\n", ptr2); }Obratite vnimanie: v dannom primere ya vospol'zovalsya "nekotorymi oslableniyami" dlya tipa vozvrashchaemogo znacheniya
D::vfun()
, i vot k chemu eto privelo:
dptr 0012FF6C D::vfun(): this=0012FF6C ptr1 0012FF6C b2ptr 0012FF70 D::vfun(): this=0012FF6C ptr2 0012FF70T.o. oba raza byla vyzvana
D::vfun()
, no vozvrashchaemoe ej znachenie zavisit ot sposoba vyzova (ptr1!=ptr2
), kak eto, sobstvenno govorya, i dolzhno byt'.
Delaetsya eto tochno tak zhe, kak uzhe bylo opisano v razdele 361 "12.2.6. Virtual'nye funkcii", tol'ko pomimo korrektirovki prinimaemogo znacheniya this
neobhodimo dopolnitel'no proizvesti korrektirovku this
vozvrashchaemogo. Ponyatno, chto virtual'nye funkcii s kovariantnym tipom vozvrata vstrechayutsya nastol'ko redko, chto realizaciya ih vyzova posredstvom rasshireniya vtbl
vryad li mozhet byt' priznana adekvatnoj. Na praktike obychno sozdayutsya special'nye funkcii-zaglushki, ch'i adresa pomeshchayutsya v sootvetstvuyushchie elementy vtbl
:
// psevdokod // original'naya D::vfun, napisannaya programmistom D* D::vfun(D *const this) { // ... } // sgenerirovannaya kompilyatorom funkciya-zaglushka dlya vyzova D::vfun() cherez // ukazatel' na bazovyj klass B2 B2* D::vfun_stub(B2 *const this) { return D::vfun(this+delta_1)+delta_2; }gde vozvrashchaemyj funkciej ukazatel' korrektiruetsya posredstvom konstanty
delta_2
, voobshche govorya, ne ravnoj delta_1
.
Podvodya itog, hochetsya otmetit', chto v obshchem sluchae vyzov virtual'noj funkcii stanovitsya vse men'she pohozh na "prosto kosvennyj vyzov funkcii". Nu, i raz uzh rech' zashla o virtual'nyh funkciyah s kovariantnym tipom vozvrata, stoit privesti sootvetstvuyushchuyu chast' standarta:
10.3. Virtual'nye funkcii [class.virtual]
D::f
zameshchaet funkciyu B::f
, tipy vozvrashchaemyh imi znachenij budut kovariantnymi, esli oni udovletvoryayut sleduyushchim usloviyam:
B::f
identichen klassu v vozvrashchaemom znachenii D::f
ili on yavlyaetsya odnoznachno opredelennym otkrytym pryamym ili kosvennym bazovym klassom vozvrashchaemogo D::f
klassa i pri etom dostupen v D
D::f
imeet te zhe ili men'shie cv-kvalifikatory, chto i klass v vozvrashchaemom znachenii B::f
.
D::f
otlichaetsya ot tipa vozvrashchaemogo znacheniya B::f
, to tip klassa v vozvrashchaemom znachenii D::f
dolzhen byt' zavershen v tochke opredeleniya D::f
ili on dolzhen byt' tipom D
. Kogda zameshchayushchaya funkciya budet vyzyvana (kak poslednyaya zamestivshaya funkciya), tip ee vozvrashchaemogo znacheniya budet (staticheski) preobrazovan v tip vozvrashchaemogo znacheniya zameshchaemoj funkcii (5.2.2). Naprimer:
class B {}; class D : private B { friend class Derived; }; struct Base { virtual void vf1(); virtual void vf2(); virtual void vf3(); virtual B* vf4(); virtual B* vf5(); void f(); }; struct No_good : public Base { D* vf4(); // oshibka: B (bazovyj klass D) nedostupen }; class A; struct Derived : public Base { void vf1(); // virtual'naya i zameshchaet Base::vf1() void vf2(int); // ne virtual'naya, skryvaet Base::vf2() char vf3(); // oshibka: nepravil'nyj tip vozvrashchaemogo znacheniya D* vf4(); // OK: vozvrashchaet ukazatel' na proizvodnyj klass A* vf5(); // oshibka: vozvrashchaet ukazatel' na nezavershennyj klass void f(); }; void g() { Derived d; Base* bp=&d; // standartnoe preobrazovanie: Derived* v Base* bp->vf1(); // vyzov Derived::vf1() bp->vf2(); // vyzov Base::vf2() bp->f(); // vyzov Base::f() (ne virtual'naya) B* p=bp->vf4(); // vyzov Derived::pf() i preobrazovanie // vozvrata v B* Derived* dp=&d; D* q=dp->vf4(); // vyzov Derived::pf(), preobrazovanie // rezul'tata v B* ne osushchestvlyaetsya dp->vf2(); // oshibka: otsutstvuet argument }
3.9.3. CV-kvalifikatory [basic.type.qualifier]
net cv-kvalifikatora | < | const |
net cv-kvalifikatora | < | volatile |
net cv-kvalifikatora | < | const volatile |
const |
< | const volatile |
volatile |
< | const volatile |
Vmeste s tem, ne stoit dumat', chto STL ne soderzhit snizhayushchih effektivnost' kompromissov. Ochevidno, chto special'no napisannyj dlya resheniya konkretnoj problemy kod budet rabotat' effektivnee, vopros v tom, naskol'ko effektivnee? Naprimer, esli nam nuzhno prosto sohranit' v pamyati zaranee neizvestnoe kolichestvo elementov, a zatem ih posledovatel'no ispol'zovat', to (odnosvyaznyj) spisok budet naibolee adekvatnoj strukturoj dannyh. Odnako STL ne soderzhit odnosvyaznyh spiskov, kak mnogo my na etom teryaem?
Rassmotrim sleduyushchij primer:
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <list> struct List { // odnosvyaznyj spisok struct Data { int val; Data* next; Data(int v, Data* n=0) : val(v), next(n) {} }; Data *head, *tail; List() { head=tail=0; } ~List() { for (Data *ptr=head, *n; ptr; ptr=n) { // udalyaem vse elementy n=ptr->next; delete ptr; } } void push_back(int v) // dobavlyaem element { if (!head) head=tail=new Data(v); else tail=tail->next=new Data(v); } }; long Count, Var; void f1() { List lst; for (int i=0; i<1000; i++) lst.push_back(i); for (List::Data* ptr=lst.head; ptr; ptr=ptr->next) Var+=ptr->val; } void f2() { typedef std::list<int> list_type; list_type lst; for (int i=0; i<1000; i++) lst.push_back(i); for (list_type::const_iterator ci=lst.begin(), cend=lst.end(); ci!=cend; ++ci) Var+=*ci; } int main(int argc, char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f1(); c2=clock(); printf("f1(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f2(); c2=clock(); printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }V nem
f1()
ispol'zuet opredelennyj nami List
: vstavlyaet 1000 elementov, a zatem prohodit po spisku.
T.k. STL ispol'zuet sobstvennyj raspredelitel' pamyati (vskore vy uvidite, chto delaet ona eto sovsem ne naprasno), to to zhe samoe sleduet poprobovat' i nam:
struct List { // odnosvyaznyj spisok struct Data { // ... // dlya sobstvennogo raspredeleniya pamyati static Data* free; static void allocate(); void* operator new(size_t); void operator delete(void*, size_t); }; // ... }; List::Data* List::Data::free; void List::Data::allocate() { const int sz=100; // vydelyaem bloki po sz elementov free=reinterpret_cast<Data*>(new char[sz*sizeof(Data)]); // sceplyaem svobodnye elementy for (int i=0; i<sz-1; i++) free[i].next=free+i+1; free[sz-1].next=0; } inline void* List::Data::operator new(size_t) { if (!free) allocate(); Data* ptr=free; free=free->next; return ptr; } inline void List::Data::operator delete(void* dl, size_t) { // dobavlyaem v nachalo spiska svobodnyh elementov Data* ptr=static_cast<Data*>(dl); ptr->next=free; free=ptr; }Obratite vnimanie, chto v dannom primere nash raspredelitel' pamyati ne vozvrashchaet poluchennuyu pamyat' sisteme. No eto ne memory leak (utechka pamyati) -- eto memory pool, t.e. zaranee vydelennyj zapas pamyati dlya bystrogo posleduyushchego ispol'zovaniya. Na pervyj vzglyad, raznica mezhdu memory leak i memory pool mozhet pokazat'sya slishkom tonkoj, no ona est': delo v tom, chto v pervom sluchae potreblenie pamyati ne ogranicheno, vplot' do polnogo ee ischerpaniya, a vo vtorom ono nikogda ne prevysit real'no zatrebovannogo programmoj ob®ema plyus nekotoraya del'ta, ne prevoshodyashchaya razmer vydelyaemogo bloka.
I eshche, nash raspredelitel' soderzhit ochen' ser'eznuyu oshibku -- on nepravil'no obrabatyvaet udalenie nulya (NULL
-ukazatelya). V nashem primere eto ne imeet znacheniya, no v real'nom kode vy obyazany eto uchest', t.e.:
inline void List::Data::operator delete(void* dl, size_t) { if (!dl) return; // ignoriruem NULL // dobavlyaem v nachalo spiska svobodnyh elementov Data* ptr=static_cast<Data*>(dl); ptr->next=free; free=ptr; }I, dlya chistoty eksperimenta, v zaklyuchenie poprobuem dvusvyaznyj spisok -- ego po pravu mozhno nazvat' vruchnuyu napisannoj al'ternativoj
std::list<int>
:
struct DList { // dvusvyaznyj spisok struct Data { int val; Data *prev, *next; Data(int v, Data* p=0, Data* n=0) : val(v), prev(p), next(n) {} // dlya sobstvennogo raspredeleniya pamyati static Data* free; static void allocate(); void* operator new(size_t); void operator delete(void*, size_t); }; Data *head, *tail; DList() { head=tail=0; } ~DList() { for (Data *ptr=head, *n; ptr; ptr=n) { // udalyaem vse elementy n=ptr->next; delete ptr; } } void push_back(int v) // dobavlyaem element { if (!head) head=tail=new Data(v); else tail=tail->next=new Data(v, tail); } };Itak, vse gotovo, i mozhno pristupat' k testirovaniyu. Dannye tri testa ya poproboval na dvuh raznyh kompilyatorah, vot rezul'tat:
odnosvyaznyj | odnosvyaznyj s sobstvennym raspredelitelem pamyati |
dvusvyaznyj s sobstvennym raspredelitelem pamyati |
||||
f1() | f2() | f1() | f2() | f1() | f2() | |
realizaciya 1 | 9.6 | 12.1 | 1.1 | 12.1 | 1.3 | 12.1 |
realizaciya 2 | 20.2 | 2.5 | 1.8 | 2.5 | 1.9 | 2.5 |
I chto zhe my zdes' vidim?
vr
inicializiruetsya konstruktorom Record()
, a kazhdyj iz s1
elementov kontejnera vi
inicializiruetsya int()
.
Inicializaciya 10 000 elementov konstruktorom po umolchaniyu ne mozhet ne vpechatlyat' -- tol'ko v ochen' redkom sluchae nuzhno imenno eto. Esli vy vydelyaete eti 10 000 elementov pro zapas, dlya posleduyushchej perezapisi, to stoit podumat' o sleduyushchej al'ternative:
vector<X> vx; // ob®yavlyaem pustoj vektor vx.reserve(10000); // rezerviruem mesto voizbezhanie "dorogih" // pereraspredelenij v push_back() // ... vx.push_back(x_work); // dobavlyaem elementy po mere nadobnostiO nej tem bolee stoit podumat', t.k. dazhe v otlichnoj realizacii STL 3.2 ot sgi konstruktor
vector<int> vi(s1);podrazumevaet yavnyj cikl zapolneniya nulyami:
for (int i=0; i<s1; i++) vi.elements[i]=0;i trebuetsya dostatochno intellektual'nyj optimizator dlya prevrashcheniya etogo cikla v vyzov
memset()
:
memset(vi.elements, 0, sizeof(int)*s1);chto znachitel'no uluchshit proizvoditel'nost' (konechno ne programmy voobshche, a tol'ko dannogo otrezka koda). Matt Austern postavlen v izvestnost', i v budushchih versiyah sgi STL mozhno ozhidat' povysheniya proizvoditel'nosti dannogo konstruktora.
Ochen' zhal', chto dorogaya redakciya sochla vozmozhnym pomestit' v knigu takuyu glupost'. Dlya privedeniya kolichestva "dorogih" pereraspredelenij k priemlemomu urovnyu O(log(N)), v STL ispol'zuetsya uvelichenie ob®ema zarezervirovannoj pamyati v poltora-dva raza, a pri prostom dobavlenii nekotorogo kolichestva (10, naprimer) my, ochevidno, poluchim O(N), chto est' ploho. Takzhe otmechu, chto dlya umen'sheniya kolichestva pereraspredelenij stoit vospol'zovat'sya reserve()
, osobenno, esli vy zaranee mozhete ocenit' predpolagaemuyu glubinu steka.
I delo ne tol'ko v opredelenii operacii "men'she", a eshche i v tom, chto char*
ne stoit ispol'zovat' v kachestve elementov STL kontejnerov voobshche: kontejner budet soderzhat' znachenie ukazatelya -- ne soderzhimoe stroki, kak kto-to po naivnosti mog polagat'. Naprimer, sleduyushchaya funkciya soderzhit ser'eznuyu oshibku:
void f(set<char*>& cset) { for (;;) { char word[100]; // schityvaem slovo v word ... cset.insert(word); // oshibka: vstavlyaem odin i tot zhe ukazatel' // na lokal'nuyu peremennuyu } }Dlya polucheniya ozhidaemogo rezul'tata sleduet ispol'zovat'
string
:
void f(set<string>& cset) { for (;;) { char word[100]; // schityvaem slovo v word ... cset.insert(word); // OK: vstavlyaem string } }Ispol'zovanie
char*
v STL kontejnerah privodit k chrezvychajno kovarnym oshibkam, t.k. inogda vse rabotaet pravil'no. Naprimer dokumentaciya k sgi STL shiroko ispol'zuet char*
v svoih uchebnyh primerah:
struct ltstr { bool operator()(const char* s1, const char* s2) const { return strcmp(s1, s2) < 0; } }; int main() { const int N = 6; const char* a[N] = {"isomer", "ephemeral", "prosaic", "nugatory", "artichoke", "serif"}; set<const char*, ltstr> A(a, a + N); // i t.d. }Dannyj primer vpolne korrekten, no stoit tol'ko vmesto staticheski razmeshchennyh strokovyh literalov ispol'zovat' lokal'no formiruemye C-stroki, kak nepriyatnosti ne zastavyat sebya zhdat'.
Otnosites' skepticheski k uchebnym primeram!
pair
.
CHestno govorya, pri pervom znakomstve s shablonami ot vseh etih mnogoslovnyh ob®yavlenij nachinaet ryabit' v glazah, i ne vsegda ponyatno, chto imenno udobno v takoj vot funkcii:
template <class T1,class T2> pair<T1,T2> std::make_pair(const T1& t1, const T2& t2) { return pair<T1,T2>(t1,t2); }A udobno sleduyushchee: Esli nam nuzhen ekzemplyar klassa-shablona, to my obyazany predostavit' vse neobhodimye dlya instanciirovaniya klassa parametry, t.k. na osnovanii argumentov konstruktora oni ne vyvodyatsya. S funkciyami-shablonami dela obstoyat poluchshe:
char c=1; int i=2; // probuem sozdat' "paru" pair(c,i); // nepravil'no -- pair<char,int> ne vyvoditsya pair<char,int>(c,i); // pravil'no make_pair(c,i); // pravil'no
operator[]()
.
Voobshche govorya, sushchestvuet, t.k. ona ob®yavlena v klasse, no, vvidu ee nekonstantnosti, primenena byt' ne mozhet -- pri popytke instanciirovaniya vy poluchite oshibku kompilyacii.
K schast'yu, eto ne tak: v dannom sluchae etot "dovol'no slozhnyj i redkij sintaksis" ne trebuetsya.
V samom dele, esli razresheno
f<int>(); // f -- funkciya-shablonto pochemu vdrug kompilyator ne mozhet pravil'no razobrat'sya s
obj.f<int>(); // f -- funkciya-shablon, chlen klassaMozhet, i razbiraetsya!
Istoricheski, neponimanie vozniklo iz-za togo, chto:
template
byl izobreten komitetom po standartizacii, a ne d-rom Straustrupom;
template
kak kvalifikator.
hash_map
.
A vot eshche odin "lyap", i net emu opravdaniya! Delo v tom, chto v standarte ponyatiya "podderzhivaemyj hash_map
" ne sushchestvuet. Eshche bol'she pikantnosti dannoj situacii pridaet tot fakt, chto v samoj STL, kotoraya yavlyaetsya osnovnoj chast'yu standartnoj biblioteki C++, hash_map
est' (i est' uzhe davno). D-r Straustrup pishet po etomu povodu, chto hash_map
prosto proglyadeli, a kogda hvatilis', to bylo uzhe pozdno -- nikakie sushchestvennye izmeneniya vnesti v standart bylo uzhe nel'zya. Nu chto zh, byvaet...
CHto zhe nam sovetuyut priznat' chitaemym i effektivnym (vprochem, k effektivnosti, teoreticheski, pretenzij dejstvitel'no net)?
list<int>::const_iterator p=find_if(c.begin(),c.end(),bind2nd(less<int>(),7));Osmelyus' predlozhit' drugoj variant:
list<int>::const_iterator p; for (p=c.begin(); p!=c.end(); ++p) if (*p<7) break;Trudno li eto napisat'? Po-vidimomu, net. YAvlyaetsya li etot yavnyj cikl menee chitaemym? Po moemu mneniyu, on dazhe prevoshodit chitaemost' primera s ispol'zovaniem
bind2nd()
. A esli nuzhno napisat' uslovie vida *p>=5 && *p<100
, chto, v principe, vstrechaetsya ne tak uzh i redko, to variant s ispol'zovaniem svyazyvatelej i find_if()
proigryvaet odnoznachno. Stoit dobavit' i chisto psihologicheskij effekt: vyzov krasivoj funkcii chasto podsoznatel'no vosprinimaetsya atomarnoj operaciej i ne lishne podcherknut', chto za krasivym fasadom poroj skryvaetsya krajne neeffektivnyj posledovatel'nyj poisk.
V celom, ya agitiruyu protiv poteri zdravogo smysla pri ispol'zovanii predostavlennogo nam pestrogo nabora svistulek i kolokol'chikov. Uvy, sleduet priznat', chto dlya skol'-nibud' slozhnogo primeneniya oni ne prednaznacheny, da i na prostom primere pol'za prakticheski ne vidna.
Teper' nemnogo pro vyzovy funkcij-chlenov dlya elementov kontejnera s pomoshch'yu mehanizma mem_fun()
. Dejstvitel'no, variant
for_each(lsp.begin(),lsp.end(),mem_fun(&Shape::draw)); // risuem vse figurypodkupaet svoim izyashchestvom. I dazhe bolee togo, predostavlyaemye
mem_fun()
vozmozhnosti dejstvitel'no mogut byt' vostrebovany, naprimer, pri realizacii nekotorogo abstraktnogo shablona razrabotki (design pattern). No za krasivym fasadom skryvaetsya vyzov funkcii cherez ukazatel' na chlen -- operaciya otnyud' ne deshevaya i daleko ne vse kompilyatory umeyut vstraivat' vyzov funkcii cherez takoj ukazatel'. Budem riskovat'?
A chto, esli nam nuzhno povernut' vse figury na zadannyj ugol? bind2nd()
, govorite? A esli na raznye ugly da prichem ne vse elementy kontejnera, i eti ugly rasschityvayutsya po slozhnomu algoritmu? Po-moemu, takoj variant v real'nyh programmah vstrechaetsya gorazdo chashche.
Vyhodit, chto i mehanizm mem_fun()
ne ochen'-to prednaznachen dlya ser'eznogo ispol'zovaniya. Izuchit' ego, konechno, stoit, a vot ispol'zovat' ili net -- reshat' vam.
Vot eto da! T.e. esli ya popytayus' udalit' element iz spiska s pomoshch'yu takogo remove()
, to vmesto udaleniya elementa ya poluchu prosto pereprisvaivanie (v srednem) poloviny ego elementov?!
Pojmite menya pravil'no, sredi privedennyh v etom razdele algoritmov budut i prakticheski poleznye, no derzhat' v standartnoj biblioteke ne tol'ko neeffektivnye, no dazhe ne sootvetstvuyushchie svoemu nazvaniyu algoritmy -- eto uzhe slishkom!
No v takom vide oni budut sovershenno neeffektivny v prilozhenii ko vstroennym tipam, ved' obshcheizvestno, chto dlya kopirovaniya bol'shih ob®emov informacii (esli bez nego dejstvitel'no nikak nel'zya obojtis') sleduet ispol'zovat' funkcii standartnoj biblioteki C memcpy()
i memmove()
. Vy nechasto ispol'zuete vektory vstroennyh tipov? Osmelyus' zametit', chto vektor ukazatelej vstrechaetsya ne tak uzh i redko i kak raz podhodit pod eto opredelenie. K schast'yu, u menya est' horoshaya novost': v kachestvennoj realizacii STL (naprimer ot sgi) vyzov operacii kopirovaniya dlya vector<int>
kak raz i privedet k effektivnomu memmove()
.
Vybor podhodyashchego algoritma proizvoditsya na etape kompilyacii s pomoshch'yu special'no opredelennogo shablona __type_traits<>
-- svojstva tipa. Kotoryj (po umolchaniyu) imeet bezopasnye nastrojki dlya slozhnyh tipov s netrivial'nymi konstruktorami/destruktorami i optimizirovannye specializacii dlya POD tipov, kotorye mozhno kopirovat' prostym peremeshcheniem blokov pamyati.
V C++ vy chasto budete vstrechat' abbreviaturu POD (Plain Old Data). CHto zhe ona oboznachaet? POD tip -- eto tip, ob®ekty kotorogo mozhno bezopasno peremeshchat' v pamyati (s pomoshch'yu memmove()
, naprimer). Dannomu usloviyu ochevidno udovletvoryayut vstroennye tipy (v tom chisle i ukazateli) i klassy bez opredelyaemoj pol'zovatelem operacii prisvaivaniya i destruktora.
Pochemu ya ob etom govoryu? Potomu chto, naprimer, ochevidnoe opredelenie klassa Date
yavlyaetsya POD tipom:
class Date { int day, mon, year; // ili dazhe long val; // yyyymmdd public: // ... };Poetomu stoit razreshit' optimizaciyu predostaviv sootvetstvuyushchuyu specializaciyu
__type_traits<>
:
template<> struct __type_traits<Date> { // ... };Tol'ko imejte vvidu:
__type_traits<>
-- ne chast' standartnoj biblioteki, raznye realizacii mogut ispol'zovat' razlichnye imena ili dazhe ne proizvodit' optimizaciyu voobshche. Izuchite to, chto est' u vas.
*
vozvrashchaet znachenie *(current-1)
...
Da, po smyslu imenno tak:
24.4.1.3.3 operator*
[lib.reverse.iter.op.star]
reference operator*() const;
Iterator tmp = current; return *--tmp;
I don't think anyone would use a reverse iterator if an iterator was an alternative, but then you never know what people might know. When you actually need to go through a sequence in reverse order a reverse iterator is often quite efficient compared to alternatives. Finally, there may not be any overhead because where the iterator is a vector the temporary isn't hard to optimize into a register use. One should measure before worrying too much about overhead.YA ne dumayu, chto by kto-to ispol'zoval obratnyj iterator tam, gde mozhno ispol'zovat' obychnyj, no my nikogda ne mozhem znat', chto dumayut drugie lyudi. Kogda vam dejstvitel'no nuzhno projti posledovatel'nost' v obratnom poryadke, obratnyj iterator yavlyaetsya vpolne priemlemoj al'ternativoj. V principe, inogda mozhno voobshche izbezhat' nakladnyh rashodov, naprimer v sluchae obratnogo prohoda po vektoru, kogda vremennaya peremennaya-iterator bez truda razmeshchaetsya v registre. V lyubom sluchae, ne stoit chrezmerno bespokoit'sya o proizvoditel'nosti ne provedya real'nyh izmerenij.
Vmeste s tem, obratnyj iterator vse-taki neset v sebe nenuzhnye nakladnye rashody, i dlya obratnogo prohoda po posledovatel'nosti luchshe ispol'zovat' obychnyj iterator s yavnym (pre)dekrementom.
I raz uzh rech' zashla o real'nyh izmereniyah, davajte ih proizvedem.
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <list> long Count, Var; typedef std::list<int> list_type; list_type lst; void f1() { for (list_type::reverse_iterator ri=lst.rbegin(), rend=lst.rend(); ri!=rend; ++ri) Var+=*ri; } void f2() { list_type::iterator i=lst.end(), beg=lst.begin(); if (i!=beg) { do { --i; Var+=*i; } while (i!=beg); } } int main(int argc, char** argv) { if (argc>1) Count=atol(argv[1]); for (int i=0; i<10000; i++) lst.push_back(i); clock_t c1, c2; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f1(); c2=clock(); printf("f1(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f2(); c2=clock(); printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }V dannom primere spisok iz 10 000 elementov prohoditsya neskol'ko tysyach raz (zadaetsya parametrom) s ispol'zovaniem obratnogo (v
f1()
) i obychnogo (v f2()
) iteratorov. Pri ispol'zovanii kachestvennogo optimizatora raznicy vremeni vypolneniya zamecheno ne bylo, a dlya "obychnyh" realizacij ona sostavila ot 45% do 2.4 raza.
I eshche odna problema: privodit li postinkrement iteratora k sushchestvennym nakladnym rashodam po sravneniyu s preinkrementom? Davajte vnesem sootvetstvuyushchie izmeneniya:
void f1() { for (list_type::iterator i=lst.begin(), end=lst.end(); i!=end; ++i) Var+=*i; } void f2() { for (list_type::iterator i=lst.begin(), end=lst.end(); i!=end; i++) Var+=*i; }I opyat' vse tot zhe rezul'tat: raznicy mozhet ne byt', a tam, gde ona proyavlyalas', ee velichina nahodilas' v predelah 5 - 30 procentov.
V celom, ne stoit ispol'zovat' potencial'no bolee dorogie obratnye iteratory i postinkrementy, esli vy ne ubedilis' v intellektual'nosti ispol'zuemogo optimizatora.
Vpolne rezonnym budet vopros: chto zhe zdes' imelos' vvidu? Nedostatok kakih svojstv meshaet ssylkam C++ byt' "sovershennymi"? D-r. Straustrup otvetil sleduyushchee:
Something that would allow a copy constructor to be defined using a user-defined reference object.CHto-to, chto pozvolilo by opredelit' konstruktor kopirovaniya s ispol'zovaniem predostavlennogo pol'zovatelem ssylochnogo tipa.
template<class T> T* Pool_alloc<T>::allocate(size_type n, void* =0) { if (n==1) return static_cast<T*>(mem_alloc()); // ... }
Kak vsegda, samoe interesnoe skryvaetsya za mnogotochiem. Kak zhe nam realizovat' chast' allocate<>()
dlya n!=1
? Prostym vyzovom v cikle mem_alloc()
? Uvy, v dannom sluchae ochevidnoe reshenie ne podhodit sovershenno. Pochemu? Davajte rassmotrim povedenie Pool_alloc<char>
. Glyadya na konstruktor original'nogo Pool
:
Pool::Pool(unsigned int sz) : esize(sz<sizeof(Link*) ? sizeof(Link*) : sz) { // ... }mozhno zametit', chto dlya
sz==sizeof(char)
dlya kazhdogo char
my budem vydelyat' sizeof(Link*)
bajt pamyati. Dlya "obychnoj" realizacii eto oznachaet chetyrehkratnyj pererashod pamyati! T.o. vydelenie pamyati dlya massivov ob®ektov tipa X
, gde sizeof(X)<sizeof(Link*)
stanovitsya netrivial'noj zadachej, ravno kak i posleduyushchee ih osvobozhdenie v deallocate<>()
, fakticheski, pridetsya principial'no izmenit' algoritm raboty allokatora.
template<class T, class A> T* temporary_dup(vector<T,A>& v) { T* p=get_temporary_buffer<T>(v.size()).first; if (p==0) return 0; copy(v.begin(),v.end(),raw_storage_iterator<T*,T>(p)); return p; }
Voobshche govorya, privedennaya funkciya napisana nekorrektno, t.k. ne proveryaetsya vtoroj element vozvrashchaemoj get_temporary_buffer<>()
pary. T.k. get_temporary_buffer<>()
mozhet vernut' men'she pamyati, chem my zaprosili, to neobhodima drugaya proverka:
template<class T, class A> T* temporary_dup(vector<T,A>& v) { pair<T*,ptrdiff_t> p(get_temporary_buffer<T>(v.size())); if (p.second<v.size()) { if (p.first) return_temporary_buffer(p.first); return 0; } copy(v.begin(),v.end(),raw_storage_iterator<T*,T>(p)); return p.first; }
assign(s,n,x)
pri pomoshchi assign(s[i],x)
prisvaivaet n
kopij x
stroke s
.compare()
ispol'zuet dlya sravneniya simvolov lt()
i eq()
.
K schast'yu, dlya obychnyh simvolov char_traits<char>
eto ne tak, v tom smysle, chto ne proishodit vyzov v cikle lt()
, eq()
, assign(s[i],x)
, a ispol'zuyutsya special'no dlya etogo prednaznachennye memcmp()
i memset()
, chto, vprochem, ne vliyaet na konechnyj rezul'tat. T.e. ispol'zuya strcmp()
my nichego ne vyigryvaem, dazhe bolee togo, v special'no provedennyh mnoj izmereniyah proizvoditel'nosti, sravneniya string
okazalis' na 30% bystree, chem prinyatoe v C sravnenie char*
s pomoshch'yu strcmp()
. CHto i ne udivitel'no: dlya string
razmery sravnivaemyh massivov char
izvestny zaranee.
basic_string
hranit dlinu stroki, ne polagayas' na zavershayushchij simvol (nol').
Vmeste s tem, horosho optimizirovannye realizacii hranyat stroku vmeste s zavershayushchim nulem, daby maksimal'no uskorit' funkciyu basic_string::c_str()
. Ne sekret, chto bol'shinstvo ispol'zuemyh funkcij (tradicionno) prinimayut stroku v vide [const
] char*
vmesto ekvivalentnogo po smyslu [const
] string&
, ishodya iz togo prostogo fakta, chto my ne mozhem uskorit' "bezopasnuyu" realizaciyu, no mozhem skryt' effektivnuyu za bezopasnym interfejsom.
K slovu skazat', moj lichnyj opyt svidetel'stvuet o tom, chto sluhi ob opasnosti manipulirovaniya prostymi char*
v stile C okazyvayutsya sil'no preuvelichennymi. Da, vy dolzhny sledit' za vsemi melochami, no, naprimer, ni u kogo ne voznikaet protesta po povodu togo, chto esli v formule kornej kvadratnogo uravneniya my vmesto '-
' napishem '+
', to rezul'tat budet neveren.
Rezyumiruya dannyj abzac, hochu skazat', chto string
ispol'zovat' mozhno i nuzhno, no esli logika raboty vashej programmy intensivno ispol'zuet manipulyacii so strokami, stoit podumat' o razrabotke sobstvennyh sredstv, osnovannyh na funkciyah tipa memcpy()
, a v "uzkih" mestah bez etogo prosto ne obojtis'.
YA by poprosil vas ser'ezno otnestis' k dannomu sovetu (t.e. k proverke imeyushchejsya realizacii). Naprimer, sgi STL 3.2 vsegda kopiruet simvoly stroki, ne polagayas' na osnovannuyu na podschete ssylok versiyu. Avtory biblioteki ob®yasnyayut eto tem, chto ispol'zuyushchie model' podscheta ssylok stroki ne podhodyat dlya mnogopotochnyh prilozhenij.
Imi utverzhdaetsya, chto ispol'zuyushchie dannuyu realizaciyu strok mnogopotochnye prilozheniya avarijno zavershayut svoyu rabotu odin raz v neskol'ko mesyacev i imenno iz-za strok. V principe, model' podscheta ssylok dejstvitel'no ploho podhodit dlya mnogopotochnyh prilozhenij, t.k. ee ispol'zovanie privodit k sushchestvennym nakladnym rashodam (bolee podrobno ob etom mozhno pochitat' u Herb Sutter Reference Counting - Part III), no vot sobstvenno avarijnoe zavershenie raboty mozhet byt' vyzvano tol'ko oshibkami v realizacii -- chudes ne byvaet.
Kak by to ni bylo, no fakt ostaetsya faktom: sushchestvuyut otlichno optimizirovannye realizacii standartnoj biblioteki, kotorye, po tem ili inym prichinam, otkazalis' ot ispol'zovaniya osnovannyh na podschete ssylok strok.
Rezyumiruya dannyj material hochu otmetit', chto ya vsegda, gde eto vozmozhno, starayus' izbegat' kopirovaniya strok, naprimer putem peredachi const string&
.
(cerr.operator<<("x=")).operator<<(x);
Konechno zhe na samom dele vse ne tak: v novyh potokah vvoda-vyvoda operator vyvoda stroki bol'she ne yavlyaetsya funkciej-chlenom, sledovatel'no ono budet interpretirovano tak:
operator<<(cerr,"x=").operator<<(x);Tovarishchi programmisty! Eshche raz povtoryu: nikogda ne kopirujte blokami staryj tekst, a esli eto vse-taki neobhodimo, -- obyazatel'no proveryajte kazhduyu zagogulinu!
Vot grazhdanin Straustrup zabyl proverit', i, v rezul'tate, novyj reliz ego monografii soderzhit ochevidnuyu oshibku.
Vynuzhden vas ogorchit': opredelennye standartom potoki C++ zayavlennym svojstvom ne obladayut. Oni vsegda rabotayut medlennee C, a v nekotoryh realizaciyah -- medlenno do smeshnogo (pravda, ob®ektivnosti radi stoit otmetit', chto mne popadalis' i sovershenno otvratitel'no realizovannye FILE*
potoki C, v rezul'tate chego C++ kod vyryvalsya vpered; no eto prosto nedorazumenie, esli ne skazat' krepche!). Rassmotrim sleduyushchuyu programmu:
#include <stdio.h> #include <time.h> #include <io.h> // dlya open() #include <fcntl.h> #include <iostream> #include <fstream> using namespace std; void workc(char*); void workcpp(char*); void work3(char*); int main(int argc, char **argv) { if (argc==3) switch (*argv[2]-'0') { case 1: { workc(argv[1]); break; } case 2: { workcpp(argv[1]); break; } case 3: { work3(argv[1]); break; } } } void workc(char* fn) { FILE* fil=fopen(fn, "rb"); if (!fil) return; time_t t1; time(&t1); long count=0; while (getc(fil)!=EOF) count++; time_t t2; time(&t2); fclose(fil); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; } void workcpp(char* fn) { ifstream fil(fn, ios_base::in|ios_base::binary); if (!fil) return; time_t t1; time(&t1); long count=0; while (fil.get()!=EOF) count++; time_t t2; time(&t2); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; } class File { int fd; // deskriptor fajla unsigned char buf[BUFSIZ]; // bufer standartnogo razmera unsigned char* gptr; // sleduyushchij chitaemyj simvol unsigned char* bend; // konec dannyh int uflow(); public: File(char* fn) : gptr(0), bend(0) { fd=open(fn, O_RDONLY|O_BINARY); } ~File() { if (Ok()) close(fd); } int Ok() { return fd!=-1; } int gchar() { return (gptr<bend) ? *gptr++ : uflow(); } }; int File::uflow() { if (!Ok()) return EOF; int rd=read(fd, buf, BUFSIZ); if (rd<=0) { // oshibka ili EOF close(fd); fd=-1; return EOF; } gptr=buf; bend=buf+rd; return *gptr++; } void work3(char* fn) { File fil(fn); if (!fil.Ok()) return; time_t t1; time(&t1); long count=0; while (fil.gchar()!=EOF) count++; time_t t2; time(&t2); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; }Ee nuzhno zapuskat' s dvumya parametrami. Pervyj parametr -- eto imya (bol'shogo) fajla dlya chteniya, a vtoroj -- cifra 1, 2 ili 3, vybirayushchaya funkciyu
workc()
, workcpp()
ili work3()
sootvetstvenno. Tol'ko ne zabud'te pro diskovyj kesh, t.e. dlya polucheniya ob®ektivnyh rezul'tatov programmu nuzhno zapustit' neskol'ko raz dlya kazhdogo iz variantov.
Neobychnym mestom zdes' yavlyaetsya funkciya work3()
i sootvetstvuyushchij ej klass File
. Oni napisany special'no dlya proverki "chestnosti" realizacii standartnyh sredstv vvoda-vyvoda C -- FILE*
. Esli vdrug okazhetsya, chto workc()
rabotaet sushchestvenno medlennee work3()
, to vy imeete polnoe pravo nazvat' sozdatelej takoj biblioteki, kak minimum, polnymi neuchami.
A sejchas poprobuem poluchit' informaciyu k razmyshleniyu: provedem seriyu kontrol'nyh zapuskov i posmotrim na rezul'tat.
I chto zhe nam govoryat bezzhalostnye cifry? Raznica v razy! A dlya odnogo shiroko rasprostranennogo kommercheskogo paketa (ne budem pokazyvat' pal'cem) ona poroj dostigala 11 raz!!!
Stoit tol'ko vzglyanut' na opredeleniya vyzyvaemyh funkcij, kak otvet srazu stanet ochevidnym.
Dlya C s ego getc()
v tipichnoj realizacii my imeem:
#define getc(f) ((--((f)->level) >= 0) ? (unsigned char)(*(f)->curp++) : _fgetc (f))T.e. koroten'kij makros vmesto funkcii. Kak govoritsya -- vsego-nichego. A vot dlya C++ standart trebuet stol'ko, chto ocherednoj raz zadaesh'sya voprosom: dumali li gospoda-komitetchiki o tom, chto gor'kie plody ih tvorchestva komu-to real'no pridetsya primenyat'?!
Nu i ladno: preduprezhden -- vooruzhen! A chto, esli zadat' bufer pobol'she?
void workc(char* fn) { // ... if (setvbuf(fil, 0, _IOFBF, LARGE_BUFSIZ)) return; // ... } void workcpp(char* fn) { // ... char* buf=new char[LARGE_BUFSIZ]; fil.rdbuf()->pubsetbuf(buf, LARGE_BUFSIZ); // ... delete [] buf; }Kak ni stranno, po suti nichego ne izmenitsya! Delo v tom, chto sovremennye OS pri rabote s diskom ispol'zuyut ochen' kachestvennye algoritmy keshirovaniya, tak chto eshche odin uroven' buferizacii vnutri prilozheniya okazyvaetsya izlishnim (v tom smysle, chto ispol'zuemye po umolchaniyu bufery potokov vpolne adekvatny).
Kstati, odnim iz horoshih primerov neobhodimosti ispol'zovaniya mnogopotochnyh programm yavlyaetsya vozmozhnost' uskoreniya raboty programm kopirovaniya fajlov, kogda ishodnyj fajl i kopiya raspolozheny na raznyh ustrojstvah. V etom sluchae programma zapuskaet neskol'ko potokov, osushchestvlyayushchih asinhronnye chtenie i zapis'. No v sovremennyh OS v etom net nikakogo smysla, t.k. predostavlyaemoe sistemoj keshirovanie krome vsego prochego obespechivaet i prozrachnoe dlya prikladnyh programm asinhronnoe chtenie i zapis'.
Podvodya itog, hochetsya otmetit', chto esli vvod-vyvod yavlyaetsya uzkim mestom vashego prilozheniya, to sleduet vozderzhat'sya ot ispol'zovaniya standartnyh potokov C++ i ispol'zovat' proverennye desyatiletiyami metody.
printf()
programmista. Ne verite? Davajte poprobuem vyvesti obyknovennuyu datu v formate dd.mm.yyyy
:
int day= 31, mon= 1, year=1974; printf("%02d.%02d.%d\n", day, mon, year); // 31.01.1974 cout<<setfill('0')<<setw(2)<<day<<'.'<<setw(2)<<mon<<setfill(' ')<<'.' <<year<<"\n"; // tozhe 31.01.1974Dumayu, chto kommentarii izlishni.
Za chto zhe ne lyubyat potoki C i chem potoki C++ mogut byt' udobnee? U potokov C++ est' tol'ko odno sushchestvennoe dostoinstvo -- tipobezopasnost'. T.k. potoki C++ vse zhe nuzhno ispol'zovat', ya napisal special'nyj manipulyator, kotoryj, ostavayas' tipobezopasnym, pozvolyaet ispol'zovat' format ...printf()
. On ne vyzyvaet sushchestvennyh nakladnyh rashodov i s ego pomoshch'yu privedennyj vyshe primer budet vyglyadet' sleduyushchim obrazom:
cout<<c_form(day,"02")<<'.'<<c_form(mon,"02")<<'.'<<year<<'\n';Vot ishodnyj kod zagolovochnogo fajla:
#include <ostream> /** lichnoe prostranstvo imen funkcii c_form, soderzhashchee detali realizacii */ namespace c_form_private { typedef std::ios_base::fmtflags fmtflags; typedef std::ostream ostream; typedef std::ios_base ios; /** * Vspomogatel'nyj klass dlya osushchestvleniya formatirovaniya. */ class Formatter { /** flagi dlya ustanovki */ fmtflags newFlags; /** shirina */ int width; /** tochnost' */ int prec; /** simvol-zapolnitel' */ char fill; /** sohranyaemye flagi */ fmtflags oldFlags; public: /** * Sozdaet ob®ekt, ispol'zuyushchij peredannoe formatirovanie. */ Formatter(const char* form, int arg1, int arg2); /** * Ustanavlivaet novoe formatirovanie dlya peredannogo potoka, sohranyaya * staroe. */ void setFormatting(ostream& os); /** * Vosstanavlivaet pervonachal'noe formatirovanie, sohranennoe v funkcii * setFormatting(). */ void restoreFormatting(ostream& os); }; /** * Vspomogatel'nyj klass. */ template <class T> class Helper { /** vyvodimoe znachenie */ const T& val; /** ob®ekt dlya formatirovaniya */ mutable Formatter fmtr; public: /** * Sozdaet ob®ekt po peredannym parametram. */ Helper(const T& val_, const char* form, int arg1, int arg2) : val(val_), fmtr(form, arg1, arg2) {} /** * Funkciya dlya vyvoda v potok sohranennogo znacheniya v zadannom formate. */ void putTo(ostream& os) const; }; template <class T> void Helper<T>::putTo(ostream& os) const { fmtr.setFormatting(os); os<<val; fmtr.restoreFormatting(os); } /** * Operator dlya vyvoda ob®ektov Helper v potok. */ template <class T> inline ostream& operator<<(ostream& os, const Helper<T>& h) { h.putTo(os); return os; } } /** * Funkciya-manipulyator, vozvrashchayushchaya ob®ekt vspomogatel'nogo klassa, dlya * kotorogo pereopredelen operator vyvoda v ostream. Pereopredelennyj operator * vyvoda osushchestvlyaet formatirovanie pri vyvode znacheniya. * @param val znachenie dlya vyvoda * @param form format vyvoda: [-|0] [chislo|*] [.(chislo|*)] [e|f|g|o|x] * @param arg1 neobyazatel'nyj argument, zadayushchij shirinu ili tochnost'. * @param arg2 neobyazatel'nyj argument, zadayushchij tochnost'. * @throws std::invalid_argument esli peredan argument form nekorrektnogo * formata. */ template <class T> inline c_form_private::Helper<T> c_form(const T& val, const char* form, int arg1=0, int arg2=0) { return c_form_private::Helper<T>(val, form, arg1, arg2); }i fajla-realizacii:
#include "c_form.hpp" #include <stdexcept> #include <cctype> namespace { /** * Vspomogatel'naya funkciya dlya chteniya desyatichnogo chisla. */ int getval(const char*& iptr) { int ret=0; do ret=ret*10 + *iptr-'0'; while (std::isdigit(*++iptr)); return ret; } } c_form_private::Formatter::Formatter(const char* form, int arg1, int arg2) : newFlags(fmtflags()), width(0), prec(0), fill(0) { const char* iptr=form; // tekushchij simvol stroki formata if (*iptr=='-') { // vyravnivanie vlevo newFlags|=ios::left; iptr++; } else if (*iptr=='0') { // dobavlyaem '0'li tol'ko esli !left fill='0'; iptr++; } if (*iptr=='*') { // chitaem shirinu, esli est' width=arg1; iptr++; arg1=arg2; // sdvigaem agrumenty vlevo } else if (std::isdigit(*iptr)) width=getval(iptr); if (*iptr=='.') { // est' tochnost' if (*++iptr=='*') { prec=arg1; iptr++; } else if (std::isdigit(*iptr)) prec=getval(iptr); else throw std::invalid_argument("c_form"); } switch (*iptr++) { case 0: return; // konec stroki formata case 'e': newFlags|=ios::scientific; break; case 'f': newFlags|=ios::fixed; break; case 'g': break; case 'o': newFlags|=ios::oct; break; case 'x': newFlags|=ios::hex; break; default: throw std::invalid_argument("c_form"); } if (*iptr) throw std::invalid_argument("c_form"); } void c_form_private::Formatter::setFormatting(ostream& os) { oldFlags=os.flags(); // ochishchaem floatfield i ustanavlivaem svoi flagi os.flags((oldFlags & ~ios::floatfield) | newFlags); if (width) os.width(width); if (fill) fill=os.fill(fill); if (prec) prec=os.precision(prec); } void c_form_private::Formatter::restoreFormatting(ostream& os) { os.flags(oldFlags); if (fill) os.fill(fill); if (prec) os.precision(prec); }Princip ego raboty osnovan na sleduyushchej idee: funkciya
c_form<>()
vozvrashchaet ob®ekt klassa c_form_private::Helper<>
, dlya kotorogo opredelena operaciya vyvoda v ostream
.
Dlya udobstva ispol'zovaniya, c_form<>()
yavlyaetsya funkciej, t.k. esli by my srazu ispol'zovali konstruktor nekotorogo klassa-shablona c_form<>
, to nam prishlos' by yavno zadavat' ego parametry:
cout<<c_form<int>(day,"02");chto, myagko govorya, neudobno. Dalee. My, v principe, mogli by ne ispol'zovat' neshablonnyj klass
Formatter
, a pomestit' ves' kod pryamo v Helper<>
, no eto privelo by k sovershenno nenuzhnoj povtornoj generacii obshchego (ne zavisyashchego ot parametrov shablona) koda.
Kak mozhno videt', realizaciyu manipulyatora c_form
vryad li mozhno nazvat' trivial'noj. Tem ne menee, izuchit' ee stoit hotya by iz teh soobrazhenij, chto v processe razrabotki bylo ispol'zovano (neozhidanno) bol'shoe kolichestvo poleznyh priemov programmirovaniya.
readsome()
yavlyaetsya operaciej nizhnego urovnya, kotoraya pozvolyaet...
T.k. privedennoe v knige opisanie readsome()
tumanno, dalee sleduet perevod sootvetstvuyushchej chasti standarta:
27.6.1.3 Funkcii neformatirovannogo vvoda [lib.istream.unformatted]
streamsize readsome(char_type* s, streamsize n);
!good()
vyzyvaet setstate(failbit)
, kotoraya mozhet vozbudit' isklyuchenie. Inache izvlekaet simvoly i pomeshchaet ih v massiv, na pervyj element kotorogo ukazyvaet s
. Esli rdbuf()->in_avail() == -1
, vyzyvaet setstate(eofbit)
(kotoraya mozhet vozbudit' isklyuchenie ios_base::failure
(27.4.4.3)) i ne izvlekaet simvoly;
rdbuf()->in_avail() == 0
, ne izvlekaet simvoly
rdbuf()->in_avail() > 0
, izvlekaet min(rdbuf()->in_avail(),n))
simvolov
Dumayu, chto stoit popodrobnee rassmotret' dannyj konkretnyj sluchaj, t.k. on illyustriruet dovol'no rasprostranennuyu oshibku proektirovaniya. Na pervyj vzglyad mozhet pokazat'sya, chto ideya sdelat' klass Circle
proizvodnym ot klassa Ellipse
yavlyaetsya vpolne priemlemoj, ved' oni svyazany otnosheniem is-a: kazhdaya okruzhnost' yavlyaetsya ellipsom. Nekorrektnost' dannoj idei stanet ochevidnoj, kak tol'ko my pristupim k napisaniyu koda.
U ellipsa, krome prochih atributov, est' dva parametra: poluosi a
i b
. I proizvodnaya okruzhnost' ih unasleduet. Bolee togo, nam nuzhen odin edinstvennyj radius dlya okruzhnosti i my ne mozhem dlya etih celej ispol'zovat' odin iz unasledovannyh atributov, t.k. eto izmenit ego smysl i poluchennyj ot ellipsa kod perestanet rabotat'. Sledovatel'no my vynuzhdeny dobavit' novyj atribut -- radius i, pri etom, podderzhivat' v korrektnom sostoyanii unasledovannye atributy. Ochevidno, chto podobnogo roda nasledovanie lisheno smysla, t.k. ne uproshchaet, a uslozhnyaet razrabotku.
V chem zhe delo? A delo v tom, chto ponyatie okruzhnost' v matematicheskom smysle yavlyaetsya ogranicheniem ponyatiya ellips, t.e. ego chastnym sluchaem. A nasledovanie budet polezno, esli konstruiruemyj nami ob®ekt soderzhit podob®ekt bazovogo klassa i vse unasledovannye operacii dlya nego imeyut smysl (rassmotrite, naprimer, operaciyu izmeneniya znacheniya poluosi b
-- ona nichego ne znaet ob invariante okruzhnosti i legko ego razrushit). Drugimi slovami, ob®ekt proizvodnogo klassa dolzhen byt' rasshireniem ob®ekta bazovogo klassa, no ne ego chastnym sluchaem (izmeneniem), t.k. my ne mozhem povliyat' na povedenie bazovogo klassa, esli on nam ne predostavil sootvetstvuyushchih vozmozhnostej, naprimer v vide podhodyashchego nabora virtual'nyh funkcij.
T.k. slozhnye ob®yavleniya C++ mogut byt' neponyatny dazhe nenovichku, stoit prokommentirovat' privedennye v knige ob®yavleniya. Neochevidnost' vseh privedennyh primerov osnovana na dobavlenii lishnih skobok:
T(*e)(int(3)); |
ekvivalentno | T* e(int(3)); |
To, chto inicializaciya ukazatelya s pomoshch'yu int zapreshchena, sintaksichestim analizatorom ne prinimaetsya vo vnimanie: budet raspoznano ob®yavlenie ukazatelya i vydana oshibka. |
T(f)[4]; |
ekvivalentno | T f[4]; |
|
T(a); |
ekvivalentno | T a; |
|
T(a)=m; |
ekvivalentno | T a=m; |
|
T(*b)(); |
ob®yavlenie ukazatelya na funkciyu. | ||
T(x),y,z=7; |
ekvivalentno | T x,y,z=7; |
template<class C> class Basic_ops { // bazovye operacii s kontejnerami friend bool operator==<>(const C&, const C&); // sravnenie elementov friend bool operator!=<>(const C&, const C&); // ... };Ugolki (
<>
) posle imen funkcij oznachayut, chto druz'yami yavlyayutsya funkcii-shablony (pozdnie izmeneniya standarta).
|tot tekst vzyat iz spiska avtorskih ispravlenij k 10 tirazhu.
Pochemu v dannom sluchae neobhodimy <>
? Potomu chto inache my ob®yavlyaem drugom operator==()
ne shablon, t.k. do ob®yavleniya klassa v okruzhayushchem kontekste ne bylo ob®yavleniya operator==()
-shablona. Vot formulirovka standarta:
14.5.3. Druz'ya [temp.friend]
template<class T> class task; template<class T> task<T>* preempt(task<T>*); template<class T> class task { // ... friend void next_time(); friend void process(task<T>*); friend task<T>* preempt<T>(task<T>*); template<class C> friend int func(C); friend class task<int>; template<class P> friend class frd; // ... };zdes' funkciya
next_time
yavlyaetsya drugom kazhdoj specializacii klassa-shablona task
; t.k. process
ne imeet yavnyh template-arguments, kazhdaya specializaciya klassa-shablona task
imeet funkciyu-druga process
sootvetstvuyushchego tipa i etot drug ne yavlyaetsya specializaciej funkcii-shablona; t.k. drug preempt
imeet yavnyj template-argument <T>
, kazhdaya specializaciya klassa-shablona task
imeet drugom sootvetstvuyushchuyu specializaciyu funkcii-shablona preempt
; i, nakonec, kazhdaya specializaciya klassa-shablona task
imeet drugom vse specializacii funkcii-shablona func
. Analogichno, kazhdaya specializaciya klassa-shablona task
imeet drugom klass-specializaciyu task<int>
, i vse specializacii klassa-shablona frd
.
template
kak kvalifikatorV dannom razdele d-r Straustrup privel primer ego ispol'zovaniya s funkciej-chlenom shablonom. A chto, esli nam nuzhno vyzvat' staticheskuyu funkciyu-chlen ili funkciyu-druga? Polnyj primer budet vyglyadet' sleduyushchim obrazom:
template <class T> void get_new3(); // (1) template <class Allocator> void f(Allocator& m) { int* p1= m.template get_new1<int>( ); int* p2=Allocator::template get_new2<int>(m); int* p3= get_new3<int>(m); } struct Alloc { template <class T> T* get_new1() { return 0; } template <class T> static T* get_new2(Alloc&) { return 0; } template <class T> friend T* get_new3(Alloc&) { return 0; } }; int main() { Alloc a; f(a); }Itak:
get_new1
--- eto funkciya-chlen, dlya vyzova kotoroj v dannom sluchae obyazatel'no dolzhen byt' ispol'zovan kvalifikator template
. Delo v tom, chto v tochke opredeleniya f
klass Allocator
yavlyaetsya vsego lish' imenem parametra shablona i kompilyatoru nuzhno podskazat', chto dannyj vyzov -- eto ne (oshibochnoe) vyrazhenie (m.get_new1) < int...
get_new2
-- eto staticheskaya funkciya-chlen, pri vyzove iz f
, ee imya dolzhno byt' predvareno vse tem zhe kvalifikatorom template
po tem zhe prichinam.
get_new3
-- drug klassa Alloc
, privnosit v nash primer nekotorye problemy. Delo v tom, chto on ispol'zuetsya v f
do ego opredeleniya v klasse Alloc
(tochno tak zhe, kak ya ispol'zuyu do ih opredeleniya funkcii get_new1
i get_new2
). CHtoby opredelenie f
bylo korrektnym, my dolzhny garantirovat', chto imya get_new3
izvestno v tochke opredeleniya f
kak imya funkcii-shablona. Daby ne ogranichivat' obshchnost' f
, ya ne ispol'zoval v tochke (1) prototip konkretnoj get_new3
-- druga klassa Alloc
, a prosto opisal (dazhe ne opredelil!) nekotoruyu funkciyu-shablon get_new3
. Ochevidno, chto ona ne mozhet byt' ispol'zovana v f
-- ona prosto delaet vyzov
p3=get_new3<int>(m);legal'nym, vnosya v oblast' vidimosti nuzhnoe imya-shablon. Obratite vnimanie, chto opisannaya v tochke (1) funkciya
get_new3
ne imeet parametrov i ne vozvrashchaet nikakogo znacheniya. |to sdelano dlya togo, chtoby ona ne prinimalas' vo vnimanie pri vybore podhodyashchej (vozmozhno peregruzhennoj) get_new3
, v tochke ee vyzova v funkcii f
.
p3=template get_new3<int>(m);K sozhaleniyu, prihoditsya konstatirovat', chto ispol'zovanie kvalifikatora
template
ne bylo v dostatochnoj mere produmano komitetom po standartizacii C++.
CHto nuzhno optimizirovat'? Kogda? I nuzhno li voobshche? V etih voprosah legko zabludit'sya, esli s samogo nachala ne vybrat' pravil'nuyu tochku zreniya. Vzglyad so storony pol'zovatelya, vse srazu stavit na svoi mesta:
Itak, analiz proveden, reshenie prinyato -- uskoryaemsya! CHto mozhet uskorit' nashu programmu? Da vse, chto ugodno; vopros postavlen nekorrektno. CHto mozhet sushchestvenno uskorit' nashu programmu? A vot nad etim uzhe stoit podumat'.
Prezhde vsego, stoit podumat' o "vneshnem" uskorenii, t.e. o ne privodyashchih k izmeneniyu ishodnogo koda dejstviyah. Samyj shirokorasprostranennyj metod -- ispol'zovanie bolee moshchnogo "zheleza". Uvy, zachastuyu eto ne samyj effektivnyj sposob. Kak pravilo, gorazdo bol'shego mozhno dobit'sya putem pravil'nogo konfigurirovaniya togo, chto est'. Naprimer, rabota s BD -- prakticheski vsegda samoe uzkoe mesto. Dolzhno byt' ochevidno, chto pravil'naya nastrojka servera BD -- eto odno iz samyh vazhnyh dejstvij i za nego vsegda dolzhen otvechat' kompetentnyj specialist. Vy budete smeyat'sya, no grubye oploshnosti adminov proishodyat slishkom chasto, chtoby ne obrashchat' na nih vnimanie (iz moej praktiki: neodnokratno vremya raboty prilozheniya umen'shalos' s neskol'kih chasov do neskol'kih minut (!) iz-za ochevidnoj komandy UPDATE STATISTICS
; fakticheski, pered analizom plana isponeniya tyazhelyh SQL-zaprosov vsegda polezno nevznachaj pointeresovat'sya aktual'nost'yu statistiki. Ne menee chastym proisshestviem yavlyaetsya "sluchajnaya poterya" indeksa vazhnoj tablicy v rezul'tate reorganizacii ili rezervnogo kopirovaniya BD).
Kol' skoro sreda ispolneniya pravil'no skonfigurirovana, stoit obratit' vnimanie neposredstvenno na kod. Ochevidno, chto maksimal'naya skorost' eskadry opredelyaetsya skorost'yu samogo medlennogo korablya. On-to nam i nuzhen. Esli "eskadroj" yavlyaetsya nabor SQL-zaprosov rabotayushchego s BD prilozheniya, to, kak pravilo, nikakih trudnostej s opredeleniem uzkih mest ne voznikaet. Trudnosti voznikayut s opredeleniem uzkih mest "obychnyh" prilozhenij.
Uzkie mesta nuzhno iskat' tol'ko s pomoshch'yu ob®ektivnyh izmerenij, t.k. intuiciya v dannoj oblasti chashche vsego ne srabatyvaet (ne stoit utverzhdat', chto ne rabotaet voobshche). Prichem izmeryat' otnositel'nuyu proizvoditel'nost' imeet smysl tol'ko pri "reliz"-nastrojkah kompilyatora (pri otklyuchennoj optimizacii uzkie mesta mogut byt' najdeny tam, gde ih net. Uvy, dannogo roda oshibki dopuskayut dazhe opytnye programmisty) i na real'nyh "vhodnyh dannyh" (tak, naprimer, otlichnye sravnitel'nye harakteristiki v sortirovke ravnomerno raspredelennyh int
, otnyut' ne garantiruyut otlichnuyu rabotu na real'nyh klyuchah real'nyh dannyh). Dejstvitel'no ser'eznym podspor'em v poiske uzkih mest yavlyayutsya profajlery -- neot®emlemaya chast' lyuboj professional'noj sredy razrabotki.
Kogda kriticheskij uchastok koda lokalizovan, mozhno pristupat' k neposredstvennomu analizu. S chego nachat'? Nachinat' nuzhno s samyh resursoemkih operacij. Kak pravilo, po trebuemomu dlya ispolneniya vremeni, operacii legko razdelyayutsya na sloi, otlichayushchiesya drug ot druga na neskol'ko poryadkov:
Kak pravilo, tol'ko v isklyuchitel'nyh sluchayah zametnogo uskoreniya raboty mozhno dostich' putem lokal'nyh uluchshenij (kotorymi pestryat drevnie nastavleniya: a+a
vmesto 2*a
, register int i;
i t.d.), sovremennye kompilyatory prekrasno spravlyayutsya s nimi bez nas (vmeste s tem, generaciya kompilyatorom nedostatochno optimal'nogo koda "v dannom konkretnom meste" vse eshche ne yavlyaetsya redkost'yu). Ser'eznye uluchsheniya obychno prinosit tol'ko izmenenie algoritma raboty.
Pervym delom stoit obratit' vnimanie na sam algoritm (klassicheskim primerom yavlyaetsya sortirovka s algoritmami O(N*N), O(N*log(N)) i O(N*M) stoimosti ili vybor podhodyashchego kontejnera). No ne popadite v lovushku! Pamyat' sovremennyh komp'yuterov uzhe ne yavlyaetsya ustrojstvom proizvol'nogo dostupa, v tom smysle, chto promah mimo kesha pri nevinnom obrashchenii po ukazatelyu mozhet obojtis' gorazdo dorozhe vyzova trivial'noj funkcii, chej kod uzhe popal v kesh. Izvestny sluchai, kogda izmenenie prohoda bol'shoj dvumernoj matricy s posledovatel'nogo postrochnogo na "obmanyvayushchij" kesh postolbcovyj zamedlyalo rabotu algoritma v neskol'ko raz!
Esli zhe principial'nyj algoritm iznachal'no optimalen, mozhno poprobovat' ispol'zovat' zamenu urovnej resursoemkosti. Klassicheskim primerom yavlyaetsya vse to zhe keshirovanie. Naprimer vmesto dorogostoyashchego schityvaniya dannyh s diska, proishodit obrashchenie k zaranee podgotovlennoj kopii v pamyati, tem samym my perehodim s pervogo urovnya na vtoroj-tretij. Stoit otmetit', chto tehnika keshirovaniya nahodit svoe primenenie ne tol'ko v rabote s vneshnimi ustrojstvami. Esli, naprimer, v igrovoj programme uzkim mestom stanovitsya vychislenie sin(x)
, to stoit podumat' ob ispol'zovanii zaranee rasschitannoj tablicy sinusov (obychno dostatochno 360 znachenij tipa int
vmesto potencial'no bolee dorogoj plavayuchej arifmetiki). Bolee "prikladnoj" primer -- eto dlinnyj switch
po tipam soobshchenij v ih obrabotchike. Esli on stal uzkim mestom, podumajte ob ispol'zovanii tablicy perehodov ili heshirovaniya (stoimost' O(1)) ili zhe special'noj drevovidnoj struktury (stoimost' O(log(N))) -- sushchestvenno luchshe O(N), obychno obespechivaemogo switch
. Nu a pro vozmozhnost' ispol'zovaniya virtual'noj funkcii vmesto switch
ya dazhe ne stanu napominat'.
Vse eti zamechaniya primenimy v ravnoj stepeni k lyubomu yazyku. Davajte posmotrim na chto stoit obratit' vnimanie programmistam na C++.
Prezhde vsego, stoit otmetit', chto vse bolee-menee sushchestvennye malen'kie hitrosti sobstvenno C++ uzhe byli rassmotreny v predydushchih primerah, tak zhe kak i skrytye nakladnye rashody. Byt' mozhet, za kadrom ostalas' tol'ko vozmozhnost' "oblegchennogo vyzova funkcii", t.k. ona yavlyaetsya ne chast'yu (standartnogo) C++, a osobennost'yu konkretnyh realizacij.
C++ kak i C pri vyzove funkcii razmeshchaet parametry v steke. T.e. imeya parametr v registre, kompilyator zanosit ego v stek, vyzyvaet funkciyu, a v tele funkcii opyat' perenosit parametr v registr. Vsego etogo mozhno izbezhat' ispol'zovav sootvetstvuyushchee soglashenie vyzova (v nekotoryh realizaciyah ispol'zuetsya zarezervirovannoe slovo _fastcall
), kogda parametry pered vyzovom razmeshchayutsya neposredstvenno v registrah, isklyuchaya tem samym nenuzhnye stekovye operacii. Naprimer v prostom teste:
void f1(int arg) { Var+=arg; } void _fastcall f2(int arg) { Var+=arg; }funkciya
f1()
rabotala na 50% medlennee. Konechno, real'nuyu vygodu iz etogo fakta mozhno poluchit' tol'ko pri massovom ispol'zovanii funkcij oblegchennogo vyzova vo vsem proekte. I eta sovershenno besplatnaya raznica mozhet byt' dostatochno sushchestvennoj.
Eshche odin nemalovazhnyj faktor -- razmer programm. Otkuda vzyalis' vse eti sovremennye megabajty? Uvy, bol'shaya ih chast' -- mertvyj kod, real'no, bolee 90% zagruzhennogo koda nikogda ne budet vyzvano! Ne beda, esli eti megabajty prosto lezhat na diske, real'nye trudnosti poyavlyayutsya, kogda vy zagruzhaete na vypolnenie neskol'ko takih monstrov. Padenie proizvoditel'nosti sistemy vo vremya vydeleniya dopolnitel'noj virtual'noj pamyati mozhet stat' prosto katastroficheskim.
Esli pri razrabotke bol'shogo proekta iznachal'no ne priderzhivat'sya politiki strogogo opredeleniya zavisimostej mezhdu ishodnymi fajlami (i ne prinimat' ser'eznyh mer dlya ih minimizacii), to v itoge, dlya uspeshnoj linkovki budet neobhodimo podklyuchit' slishkom mnogo musora iz standartnogo instrumentariya dannogo proekta. V neskol'ko raz bol'she, chem poleznogo koda. Iz-za chego eto proishodit? Esli funkciya f()
iz file1.cpp
vyzyvaet g()
iz file2.cpp
, to, ochevidno, my obyazany podklyuchit' file2.cpp
k nashemu proektu. Pri etom, esli ne bylo prinyato special'nyh mer, to v file2.cpp
pochti vsegda najdetsya kakaya-nibud' g2()
, kak pravilo ne nuzhnaya dlya raboty g()
i vyzyvayushchaya funkcii eshche kakogo-libo fajla; i poshlo-poehalo... A kogda kazhdoe prilozhenie soderzhit svyshe polusotni ishodnyh fajlov, a v proekte neskol'ko soten prilozhenij, to navesti poryadok postfaktum uzhe ne predstavlyaetsya vozmozhnym.
Otlichnoe obsuzhdenie lokal'nyh priemov optimizacii mozhno najti u Paul Hsieh "Programming Optimization". Ne ochen' glubokij, a mestami i otkrovenno "slabyj", no, tem ne menee, prakticheski poleznyj obzor bolee vysokourovnevyh tehnik predstavlen v knige Steve Heller "Optimizing C++".
YA polnost'yu soglasen s tem, chto chrezmernoe i neobdumannoe ispol'zovanie makrosov mozhet vyzvat' bol'shie nepriyatnosti, osobenno pri povtornom ispol'zovanii koda. Vmeste s tem, ya ne znayu ni odnogo sredstva C++, kotoroe moglo by prinesti pol'zu pri chrezmernom i neobdumannom ego ispol'zovanii.
Itak, kogda makrosy mogut prinesti pol'zu?
_VAL_
, vyvodyashchij imya i znachenie peremennoj:
#define _VAL_(var) #var "=" << var << " "Nad®yazykovoj chast'yu zdes' yavlyaetsya rabota s peremennoj kak s tekstom, putem perevoda imeni peremennoj (ono sushchestvuet tol'ko v ishodnom kode programmy) v strokovyj literal, real'no sushchestvuyushchij v kode binarnom. Dannuyu vozmozhnost' mogut predostavit' tol'ko makrosy.
_ADD_
. Naprimer:
cout<<_ADD_("Oshibka chteniya");vyvedet chto-to vrode
Oshibka chteniya <file.cpp:34>A esli nuzhen perevod stroki, to stoit poprobovat'
cout<<"Oshibka chteniya" _ADD_("") "\n";Takoj metod rabotaet, potomu chto makros
_ADD_
vozvrashchaet strokovyj literal. Vrode by ekvivalentnaya funkciya
char* _ADD_(char*);vpolne podoshla by dlya pervogo primera, no ne dlya vtorogo. Konechno, dlya vyvoda v
cout
eto ne imeet nikakogo znacheniya, no v sleduyushchem punkte ya pokazhu principial'nuyu vazhnost' podobnogo povedeniya.
Rassmotrim ustrojstvo _ADD_
:
#define _ADD_tmp_tmp_(str,arg) str " <" __FILE__ ":" #arg ">" #define _ADD_tmp_(str,arg) _ADD_tmp_tmp_(str,arg) #define _ADD_(str) _ADD_tmp_(str,__LINE__)Pochemu vse tak slozhno? Delo v tom, chto
__LINE__
v otlichie ot __FILE__
yavlyaetsya chislovym, a ne strokovym literalom i chtoby privesti ego k nuzhnomu tipu pridetsya proyavit' nekotoruyu smekalku. My, konechno, ne mozhem napisat':
#define _ADD_(str) str " <" __FILE__ ":" #__LINE__ ">"t.k.
#
mozhet byt' primenen tol'ko k argumentu makrosa. Resheniem yavlyaetsya peredacha __LINE__
v vide parametra nekotoromu vspomogatel'nomu makrosu, no ochevidnoe
#define _ADD_tmp_(str,arg) str " <" __FILE__ ":" #arg ">" #define _ADD_(str) _ADD_tmp_(str,__LINE__)ne rabotaet: rezul'tatom
_ADD_("Oshibka chteniya")
budet
"Oshibka chteniya <file.cpp:__LINE__>"chto netrudno bylo predvidet'. V itoge my prihodim k privedennomu vyshe variantu, kotoryj obrabatyvaetsya preprocessorom sleduyushchim obrazom:
_ADD_("Oshibka chteniya")
posledovatel'no podstavlyaetsya v
_ADD_tmp_("Oshibka chteniya",__LINE__) _ADD_tmp_tmp_("Oshibka chteniya",34) "Oshibka chteniya" " <" "file.cpp" ":" "34" ">" "Oshibka chteniya <file.cpp:34>"
DB::Query
s sootvetstvuyushchej funkciej
void DB::Query::Statement(const char *);i my hotim vybrat' vse stroki nekotoroj tablicy, imeyushchie ravnoe nekomu "magicheskomu chislu" pole
somefield
:
#define FieldOK 7 // ... DB::Int tmp(FieldOK); q.Statement(" SELECT * " " FROM sometable " " WHERE somefield=? " ); q.SetParam(), tmp;Izlishne mnogoslovno. Kak by eto nam ispol'zovat'
FieldOK
napryamuyu? Nedostatochno znakomye s vozmozhnostyami makrosov programmisty delayut eto tak:
#define FieldOK 7 // ... #define FieldOK_CHAR "7" // ... q.Statement(" SELECT * " " FROM sometable " " WHERE somefield=" FieldOK_CHAR );V rezul'tate chego vy poluchaete vse prelesti sinhronizacii izmenenij vzaimosvyazannyh naborov makrosov so vsemi vytekayushchimi iz etogo oshibkami. Pravil'nym resheniem budet
#define FieldOK 7 // ... q.Statement(" SELECT * " " FROM sometable " " WHERE somefield=" _GETSTR_(FieldOK) );gde
_GETSTR_
opredelen sleduyushchim obrazom:
#define _GETSTR_(arg) #argKstati, privedennyj primer naglyadno demonstriruet nevozmozhnost' polnost'yu ekvivalentnoj zameny vseh chislovyh makrosov na prinyatye v C++
const int FieldOK=7; enum { FieldOK=7 };makros
_GETSTR_
ne smozhet s nimi rabotat'.
struct Table1 { // predstavlenie dannyh tablicy DB::Date Field1; DB::Int Field2; DB::Short Field3; }; void f() { Table1 tbl; DB::Query q; q.Statement(" SELECT Field1, Field2, Field3 " " FROM Table1 " ); q.BindCol(), tbl.Field1, tbl.Field2, tbl.Field3; // ... }I etot metod dejstvitel'no rabotaet. No chto, esli predstavlenie tablicy izmenilos'? Teper' nam pridetsya iskat' i ispravlyat' vse podobnye mesta -- chrezvychajno utomitel'nyj process! Ob etom stoilo pozabotit'sya zaranee:
#define TABLE1_FLD Field1, Field2, Field3 #define TABLE1_FLD_CHAR "Field1, Field2, Field3" struct Table1 { // predstavlenie dannyh tablicy DB::Date Field1; DB::Int Field2; DB::Short Field3; // vspomogatel'naya funkciya void BindCol(DB::Query& q) { q.BindCol(), TABLE1_FLD; } }; void f() { Table1 tbl; DB::Query q; q.Statement(" SELECT " TABLE1_FLD_CHAR " FROM Table1 " ); tbl.BindCol(q); // ... }Teper' izmenenie struktury tablicy obojdetsya bez zubovnogo skrezheta. Stoit otmetit', chto v opredelenii
TABLE1_FLD_CHAR
ya ne mog ispol'zovat' ochevidnoe _GETSTR_(TABLE1_FLD)
, t.k. TABLE1_FLD
soderzhit zapyatye. K sozhaleniyu, dannoe pechal'noe ogranichenie v primitivnom preprocessore C++ nikak nel'zya obojti.
q.Statement(" SELECT Field1, AccA_bal, AccA_cur, AccA_key, AccA_brn, " " AccA_per, Field2 " " FROM Table1 " ); q.BindCol(), tbl.Field1, tbl.AccA.bal, tbl.AccA.cur, tbl.AccA.key, tbl.AccA.brn, tbl.AccA.per, tbl.Field2; // ...Mozhete sebe predstavit', skol'ko pisaniny trebuetsya dlya vybora chetyreh schetov (
tbl.AccA
, tbl.AccB
, tbl.KorA
, tbl.KorB
). I snova na pomoshch' prihodyat makrosy:
#define _SACC_(arg) #arg"_bal, "#arg"_cur, "#arg"_key, "#arg"_brn, " \ #arg"_per " #define _BACC_(arg) arg.bal, arg.cur, arg.key, arg.brn, arg.per // ... q.Statement(" SELECT Field1, " _SACC_(AccA) " , Field2 " " FROM Table1 " ); q.BindCol(), tbl.Field1, _BACC_(tbl.AccA), tbl.Field2; // ...Dumayu, chto kommentarii izlishni.
struct A { MyDate Date; int Field2; short Field3; };My ne mozhem ispol'zovat' identifikator
Date
dlya imeni stolbca tablicy, t.k. DATE
yavlyaetsya zarezervirovannym slovom SQL. |ta problema legko obhoditsya s pomoshch'yu pripisyvaniya nekotorogo prefiksa:
struct TableA { DB::Date xDate; DB::Int xField2; DB::Short xField3; TableA& operator=(A&); void Clear(); };A teper' opredelim funkcii-chleny:
TableA& TableA::operator=(A& a) { xDate=ToDB(a.Date); xField2=ToDB(a.Field2); xField3=ToDB(a.Field3); return *this; } void TableA::Clear() { xDate=""; xField2=""; xField3=""; }Garantiruyu, chto esli
TableA
soderzhit hotya by paru-trojku desyatkov polej, to napisanie podobnogo koda vam ochen' bystro naskuchit, myagko govorya! Nel'zya li eto sdelat' odin raz, a potom ispol'zovat' rezul'taty? Okazyvaetsya mozhno:
TableA& TableA::operator=(A& a) { // ispol'zuem sklejku leksem: ## #define ASS(arg) x##arg=ToDB(a.arg); ASS(Date); ASS(Field2); ASS(Field3); #undef ASS return *this; } void TableA::Clear() { #define CLR(arg) x##arg="" CLR(Date); CLR(Field2); CLR(Field3); #undef CLR }Teper' opredelenie
TableA::Clear()
po TableA::operator=()
ne neset nikakoj nudnoj raboty, esli, konechno, vash tekstovyj redaktor podderzhivaet komandy poiska i zameny. Tak zhe prosto mozhno opredelit' i obratnoe prisvaivanie: A& A::operator=(TableA&)
.
Nikakaya chast' dannogo materiala ne mozhet byt' ispol'zovana v kommercheskih celyah bez pis'mennogo razresheniya avtora.
Last-modified: Thu, 07 Dec 2006 05:46:12 GMT