Ocenite etot tekst:


---------------------------------------------------------------
 © Copyright Sergej Derevyago, 2000
 Versiya ispravlennaya i dopolnennaya, 12 Oct 2004
 Origin: http://ders.stml.net/cpp/
---------------------------------------------------------------

Sergej Derevyago

C++ 3rd: kommentarii


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

Vvedenie

Vashemu vnimaniyu predlagaetsya "eshche odna" kniga po C++. CHto v nej est'? V nej est' vse, chto nuzhno dlya glubokogo ponimaniya C++. Delo v tom, chto prakticheski ves' material stoit na blestyashchej knige B.Straustrupa "YAzyk programmirovaniya C++", 3e izdanie. YA absolyutno uveren, chto interesuyushchijsya C++ programmist obyazan prochitat' "YAzyk programmirovaniya C++", a posle prochteniya on vryad li zahochet perechityvat' opisanie C++ u drugih avtorov -- maloveroyatno, chto kto-to napishet sobstvenno o C++ luchshe d-ra Straustrupa. Moya kniga soderzhit ispravleniya, kommentarii i dopolneniya, no nigde net povtoreniya uzhe izlozhennogo materiala.

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.


Str.43: 1.3.1. |ffektivnost' i struktura

Za isklyucheniem operatorov 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".


Str.73: 2.5.5. Virtual'nye funkcii

Trebovaniya po pamyati sostavlyayut odin ukazatel' na kazhdyj ob®ekt klassa s virtual'nymi funkciyami, plyus odna 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
b1
Esli 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
b2
Pochemu 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.

Str.79: 2.7.2. Obobshchennye algoritmy

Vstroennye v C++ tipy nizkogo urovnya, takie kak ukazateli i massivy, imeyut sootvetstvuyushchie operacii, poetomu my mozhem zapisat':
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 to vc1+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 ekvivalentno vc1+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 dereference vc1+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 razymenovanie vc1+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 &.


Str.128: 5.1.1. Nol'

Esli vy chuvstvuete, chto prosto obyazany opredelit' 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).

Str.192: 7.4. Peregruzhennye imena funkcij

Process poiska podhodyashchej funkcii iz mnozhestva peregruzhennyh zaklyuchaetsya v...

Privedennyj v knige punkt [2] nuzhno zamenit' na:

  1. Sootvetstvie, dostigaemoe "prodvizheniem" ("povysheniem v chine") integral'nyh tipov (naprimer, bool v int, char v int, short v int; § B.6.1), float v double.
Takzhe sleduet otmetit', chto dostupnost' funkcij-chlenov ne vliyaet na process poiska podhodyashchej funkcii, naprimer:
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.

Str.199: 7.6. Neukazannoe kolichestvo argumentov

Do vyhoda iz funkcii, gde byla ispol'zovana 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.


Str.202: 7.7. Ukazatel' na funkciyu

Prichina v tom, chto razreshenie ispol'zovaniya 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*.


Str.296: 10.4.6.2. CHleny-konstanty

Mozhno proinicializirovat' chlen, yavlyayushchijsya staticheskoj konstantoj integral'nogo tipa, dobaviv k ob®yavleniyu chlena konstantnoe vyrazhenie v kachestve inicializiruyushchego znacheniya.

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.

Str.297: 10.4.7. Massivy

Ne sushchestvuet sposoba yavnogo ukazaniya argumentov konstruktora (za isklyucheniem ispol'zovaniya spiska inicializacii) pri ob®yavlenii massiva.

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]

  1. Klass mozhet byt' opredelen vnutri funkcii; takoj klass nazyvaetsya lokal'nym (local) klassom. Imya lokal'nogo klassa yavlyaetsya lokal'nym v okruzhayushchem kontekste (enclosing scope). Lokal'nyj klass nahoditsya v okruzhayushchem kontekste i imeet tot zhe dostup k imenam vne funkcii, chto i u samoj funkcii. Ob®yavleniya v lokal'nom klasse mogut ispol'zovat' tol'ko imena tipov, staticheskie peremennye, extern peremennye i funkcii, perechisleniya iz okruzhayushchego konteksta. Naprimer:
    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
  2. Okruzhayushchaya funkciya nikakih special'nyh prav dostupa k chlenam lokal'nogo klassa ne imeet, ona podchinyaetsya obychnym pravilam (sm. razdel 11 [class.access]). Funkcii-chleny lokal'nogo klassa, esli oni voobshche est', dolzhny byt' opredeleny vnutri opredeleniya klassa.
  3. Vlozhennyj klass 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.
  4. Lokal'nyj klass ne mozhet imet' staticheskih dannyh-chlenov.

Str.316: 11.3.1. Operatory-chleny i ne-chleny

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.

Str.328: 11.5.1. Poisk druzej

Privedennyj v konce dannoj stranicy primer nuzhno zamenit' na:
// 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.

Str.333: 11.7.1. YAvnye konstruktory

Raznica mezhdu
String s1='a';  // oshibka: net yavnogo preobrazovaniya char v String
String s2(10);  // pravil'no: stroka dlya hraneniya 10 simvolov
mozhet 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:
  1. Ob®ekt b uzhe yavlyaetsya ob®ektom klassa X. V etom sluchae my poluchim neposredstvennyj vyzov konstruktora kopirovaniya:
    X a(b);
  2. Ob®ekt 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.
Eshche odna tonkost' sostoit v tom, chto v opredelennyh usloviyah realizaciyam razresheno ne sozdavat' vremennye ob®ekty:

12.8 Kopirovanie ob®ektov klassov [class.copy]

  1. Tam, gde vremennyj ob®ekt kopiruetsya posredstvom konstruktora kopirovaniya, i dannyj ob®ekt i ego kopiya imeyut odin i tot zhe tip (ignoriruya cv-kvalifikatory), realizacii razresheno schitat', chto i original i kopiya ssylayutsya na odin i tot zhe ob®ekt i voobshche ne osushchestvlyat' kopirovanie, dazhe esli konstruktor kopirovaniya ili destruktor imeyut pobochnye effekty. Esli funkciya vozvrashchaet ob®ekty klassov i 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'.
Davajte ne polenimsya i napishem malen'kij klass, pozvolyayushchij otsledit' voznikayushchie pri etom speceffekty.
#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()

Str.337: 11.9. Vyzov funkcii

Funkciya, kotoraya vyzyvaetsya povtorno, -- eto 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.

Str.344: 11.12. Klass String

Obratite vnimanie, chto dlya nekonstantnogo ob®ekta, 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.

Str.351: 12.2. Proizvodnye klassy

Bazovyj klass inogda nazyvayut superklassom, a proizvodnyj -- podklassom. Odnako podobnaya terminologiya vvodit v zabluzhdenie lyudej, kotorye zamechayut, chto dannye v ob®ekte proizvodnogo klassa yavlyayutsya nadmnozhestvom dannyh bazovogo klassa.

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).


Str.361: 12.2.6. Virtual'nye funkcii

Stoit pomnit', chto tradicionnoj i ochevidnoj realizaciej vyzova virtual'noj funkcii yavlyaetsya prosto kosvennyj vyzov funkcii...

|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=0x283fee8
T.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:

  1. V tablicu 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 vfun
    Sushchestvennym 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.
  2. Bolee effektivnym resheniem yavlyaetsya sozdanie neskol'kih tochek vhoda dlya odnoj i toj zhe virtual'noj funkcii, kazhdaya iz kotoryh sootvetstvuyushchim obrazom korrektiruet znachenie 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 vfun
    V 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.
Interesnoe i dostatochno podrobnoe opisanie predstavleniya ob®ektov i realizacii mehanizma vyzova virtual'nyh funkcij mozhno najti v stat'e C++: Under the Hood. Ona opisyvaet realizaciyu, ispol'zovannuyu razrabotchikami MSVC.

Str.382: 13.2.3. Parametry shablonov

V chastnosti, strokovyj literal ne dopustim v kachestve argumenta shablona.

Potomu chto strokovyj literal -- eto ob®ekt s vnutrennej komponovkoj (internal linkage).


Str.399: 13.6.2. CHleny-shablony

Lyubopytno, chto konstruktor shablona nikogda ne ispol'zuetsya dlya generacii kopiruyushchego konstruktora (tak, chtoby pri otsutstvii yavno ob®yavlennogo kopiruyushchego konstruktora, generirovalsya by kopiruyushchij konstruktor po umolchaniyu).

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".


Str.419: 14.4.1. Ispol'zovanie konstruktorov i destruktorov

Itak, tam, gde goditsya podobnaya prostaya model' vydeleniya resursov, avtoru konstruktora net neobhodimosti pisat' yavnyj kod obrabotki isklyuchenij.

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"...


Str.421: 14.4.2. auto_ptr

V standartnom zagolovochnom fajle <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.

Str.422: 14.4.4. Isklyucheniya i operator new

Pri nekotorom ispol'zovanii etogo sintaksisa vydelennaya pamyat' zatem osvobozhdaetsya, pri nekotorom -- net.

T.k. privedennye v knige ob®yasneniya nemnogo tumanny, vot sootvetstvuyushchaya chast' standarta:

5.3.4. New [expr.new]

  1. Esli inicializaciya ob®ekta zavershaetsya iz-za vozbuzhdeniya isklyucheniya i mozhet byt' najdena podhodyashchaya funkciya osvobozhdeniya pamyati, ona vyzyvaetsya dlya osvobozhdeniya vydelennoj dlya razmeshcheniya ob®ekta pamyati, a samo isklyuchenie peredaetsya okruzhayushchemu kontekstu. Esli podhodyashchaya funkciya osvobozhdeniya ne mozhet byt' odnoznachno opredelena, osvobozhdenie vydelennoj pamyati ne proizvoditsya (eto udobno, kogda funkciya vydeleniya pamyati na samom dele pamyat' ne vydelyaet; esli zhe pamyat' byla vydelena, to, veroyatno, proizojdet utechka pamyati).

Str.431: 14.6.1. Proverka specifikacij isklyuchenij

Specifikaciya isklyuchenij ne yavlyaetsya chast'yu tipa funkcii, i 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:

Eshche odnim neudachnym resheniem yavlyaetsya vozmozhnost' vozbuzhdat' isklyucheniya lyubyh (dazhe vstroennyh!) tipov. Pravil'nym resheniem yavlyaetsya vvedenie special'nogo bazovogo klassa dlya vseh vozbuzhdaemyh isklyuchenij s iznachal'no zalozhennoj v nem specificheskoj funkcional'nost'yu.

Str.431: 14.6.3. Otobrazhenie isklyuchenij

V nastoyashchee vremya standart ne podderzhivaet otobrazhenie isklyuchenij v 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 to std::bad_exception for exceptions thrown explicitly within an unexpected() function. This makes std::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 funkcii unexpected(). |to lishaet std::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]

  1. Esli funkciya so specifikaciej isklyuchenij vozbuzhdaet isklyuchenie ne prinadlezhashchee ee specifikacii, budet vyzvana funkciya
    	void unexpected();
    srazu zhe posle zaversheniya raskrutki steka (stack unwinding).
  2. Funkciya 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.
  3. Takim obrazom, specifikaciya isklyuchenij garantiruet, chto mogut byt' vozbuzhdeny tol'ko perechislennye isklyucheniya. Esli specifikaciya isklyuchenij soderzhit klass std::bad_exception, to lyuboe neopisannoe isklyuchenie mozhet byt' zameneno na std::bad_exception vnutri unexpected().

Str.460: 15.3.2. Dostup k bazovym klassam

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 baza
T.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.


Str.461: 15.3.2.1. Mnozhestvennoe nasledovanie i upravlenie dostupom

... dostup razreshen tol'ko v tom sluchae, esli on razreshen po kazhdomu iz vozmozhnyh putej.

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.


Str.475: 15.5. Ukazateli na chleny

Poetomu ukazatel' na virtual'nyj chlen mozhno bezopasno peredavat' iz odnogo adresnogo prostranstva v drugoe...

|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= 00401160
Prezhde 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?
  1. Vo vseh treh realizaciyah ukazatel' na chlen-dannye yavlyaetsya smeshcheniem -- ne pryamym adresom. |to vpolne logichno i iz etogo sleduet, chto eti ukazateli mozhno bezopasno peredavat' iz odnogo adresnogo prostranstva v drugoe.
  2. Ukazateli na nevirtual'nye funkcii chleny yavlyayutsya ili prosto ukazatelem na funkciyu, ili soderzhat takoj ukazatel' v kachestve odnogo iz fragmentov. Ochevidno, chto ih peredavat' v drugoe adresnoe prostranstvo nel'zya. Vprochem, v etom takzhe net nichego neozhidannogo.
  3. A teper' samoe interesnoe -- ukazateli na virtual'nye funkcii-chleny. Kak vy mozhete videt', tol'ko u odnogo iz treh kompilyatorov oni poluchilis' pohozhimi na "peredavaemye" -- u vtorogo.
Itak, ukazateli na virtual'nye funkcii-chleny mozhno bezopasno peredavat' v drugoe adresnoe prostranstvo chrezvychajno redko. I eto pravil'no! Delo v tom, chto v opredelenie C++ zakralas' oshibka: ukazateli na obychnye i virtual'nye chleny dolzhny byt' raznymi tipami. Tol'ko v etom sluchae mozhno obespechit' optimal'nost' realizacii.

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.

Str.477: 15.6. Svobodnaya pamyat'

// polagaem, chto 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.

Str.478: 15.6. Svobodnaya pamyat'

V principe, osvobozhdenie pamyati osushchestvlyaetsya togda vnutri destruktora (kotoryj znaet razmer).

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.

Str.479: 15.6.1. Vydelenie pamyati pod massiv

Na etom punkte stoit ostanovit'sya popodrobnee, tak kak s operatorom 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.

Str.480: 15.6.2. "Virtual'nye konstruktory"

... dopuskayutsya nekotorye oslableniya po otnosheniyu k tipu vozvrashchaemogo znacheniya.

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    0012FF70
T.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]

  1. Tip vozvrashchaemogo znacheniya zameshchayushchej funkcii mozhet byt' ili identichen tipu zameshchaemoj funkcii ili byt' kovariantnym (covariant). Esli funkciya D::f zameshchaet funkciyu B::f, tipy vozvrashchaemyh imi znachenij budut kovariantnymi, esli oni udovletvoryayut sleduyushchim usloviyam:
    • oni oba yavlyayutsya ukazatelyami ili ssylkami na klass (mnogourovnevye ukazateli ili ssylki na mnogourovnevye ukazateli zapreshcheny)
    • klass v vozvrashchaemom znachenii 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
    • kak ukazateli tak i ssylki imeyut identichnye cv-kvalifikatory i, pri etom, klass vozvrashchaemogo znacheniya D::f imeet te zhe ili men'shie cv-kvalifikatory, chto i klass v vozvrashchaemom znachenii B::f.
    Esli tip vozvrashchaemogo znacheniya 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
    }
A chto oznachaet zagadochnaya fraza "men'shie cv-kvalifikatory"?

3.9.3. CV-kvalifikatory [basic.type.qualifier]

  1. Mnozhestvo cv-kvalifikatorov yavlyaetsya chastichno uporyadochennym:

    net cv-kvalifikatora < const
    net cv-kvalifikatora < volatile
    net cv-kvalifikatora < const volatile
    const < const volatile
    volatile < const volatile


Str.498: 16.2.3. STL-kontejnery

Ona yavilas' rezul'tatom celenapravlennogo poiska beskompromissno effektivnyh obshchih algoritmov.

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?

Itak, nashi izmereniya pokazyvayut, chto beskompromissnaya effektivnost' STL yavlyaetsya mifom. Dazhe bolee togo, esli vy ispol'zuete nedostatochno horoshij optimizator, to ispol'zovanie STL vyzovet sushchestvennye nakladnye rashody.

Str.505: 16.3.4. Konstruktory

To est' kazhdyj iz 10 000 elementov 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 nadobnosti
O 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.

Str.508: 16.3.5. Operacii so stekom

Snoska: To est' pamyat' vydelyaetsya s nekotorym zapasom (obychno na desyat' elementov). -- Primech. red.

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.


Str.526: 17.1.4.1. Sravneniya

Takim obrazom, pri ispol'zovanii v kachestve klyuchej C-strok associativnye kontejnery budut rabotat' ne tak, kak ozhidalo by bol'shinstvo lyudej.

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!


Str.541: 17.4.1.2. Iteratory i pary

Takzhe obespechena funkciya, pozvolyayushchaya udobnym obrazom sozdavat' 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

Str.543: 17.4.1.3. Indeksaciya

Poetomu dlya konstantnyh associativnyh massivov ne sushchestvuet versii 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.


Str.555: 17.5.3.3. Drugie operacii

K sozhaleniyu, vyzov yavno kvalificirovannogo shablona chlena trebuet dovol'no slozhnogo i redkogo sintaksisa.

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-shablon
to pochemu vdrug kompilyator ne mozhet pravil'no razobrat'sya s
obj.f<int>();  // f -- funkciya-shablon, chlen klassa
Mozhet, i razbiraetsya!

Istoricheski, neponimanie vozniklo iz-za togo, chto:

  1. neposredstvenno etot tumannyj aspekt ispol'zovaniya kvalifikatora template byl izobreten komitetom po standartizacii, a ne d-rom Straustrupom;
  2. pervym kompilyatorom, podderzhivayushchim eksperimental'nye (na tot moment) novovvedeniya, byl aC++ ot HP. Dannyj kompilyator oshibochno treboval nalichiya kvalifikatora, chto, vkupe s neochevidnym tekstom standarta, ne moglo ne vvesti v zabluzhdenie.
Dal'nejshee razvitie temy "slozhnogo i redkogo sintaksisa" mozhno najti v razdele B.13.6. template kak kvalifikator.

Str.556: 17.6. Opredelenie novogo kontejnera

... a potom primenyajte podderzhivaemyj 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...


Str.583: 18.4.4.1. Svyazyvateli

CHitaemo? |ffektivno?

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.


Str.584: 18.4.4.2. Adaptery funkcij-chlenov

Snachala rassmotrim tipichnyj sluchaj, kogda my hotim vyzvat' funkciyu-chlen bez argumentov...

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 figury
podkupaet 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.


Str.592: 18.6. Algoritmy, modificiruyushchie posledovatel'nost'

Vmesto vstavki i udaleniya elementov takie algoritmy izmenyayut znacheniya elementov...

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!


Str.592: 18.6.1. Kopirovanie

Opredeleniya bazovyh operacij kopirovaniya trivial'ny...

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.

Str.622: 19.2.5. Obratnye iteratory

|to privodit k tomu, chto * vozvrashchaet znachenie *(current-1)...

Da, po smyslu imenno tak:

24.4.1.3.3 operator* [lib.reverse.iter.op.star]

reference operator*() const;
  1. Dejstviya:
    Iterator tmp = current;
    return *--tmp;
T.e. kazhdyj raz, kogda vy primenyaete razymenovanie obratnogo iteratora, proishodit sozdanie vremennogo iteratora, ego dekrement i razymenovanie. Ne mnogovato li, dlya takoj prostoj i chasto ispol'zuemoj (kak pravilo, v cikle dlya kazhdogo elementa) operacii? D-r Straustrup pishet po etomu povodu sleduyushchee:

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.


Str.634: 19.4.1. Standartnyj raspredelitel' pamyati

Naprimer, v ramkah yazyka C++ nevozmozhno opredelit' sovershennyj ssylochnyj tip.

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.


Str.637: 19.4.2. Raspredeliteli pamyati, opredelyaemye pol'zovatelem

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.

Str.641: 19.4.4. Neinicializirovannaya pamyat'

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;
}

Str.647: 20.2.1. Osobennosti simvolov

Vyzov assign(s,n,x) pri pomoshchi assign(s[i],x) prisvaivaet n kopij x stroke s.
Funkciya 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.


Str.652: 20.3.4. Konstruktory

Realizaciya 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'.


Str.655: 20.3.6. Prisvaivanie

|to delaet ispol'zovanie strok, kotorye tol'ko schityvayutsya i zadayutsya v kachestve argumenta, gorazdo bolee deshevym, chem kto-to mog po naivnosti predpolozhit'. Odnako bylo by tak zhe naivno so storony programmistov ne proveryat' imeyushchiesya u nih realizacii pered napisaniem koda, kotoryj polagaetsya na optimizaciyu kopirovaniya strok.

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&.


Str.676: 21.2.2. Vyvod vstroennyh tipov

... budet interpretirovano tak:
(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.


Str.687: 21.3.4. Vvod simvolov

Kak uzhe bylo skazano, glavnaya sila yazyka C -- v ego sposobnosti schityvat' simvoly i reshat', chto s nimi nichego ne nado delat' -- prichem vypolnyat' eto bystro. |to dejstvitel'no vazhnoe dostoinstvo, kotoroe nel'zya nedoocenivat', i cel' C++ -- ne utratit' ego.

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.


Str.701: 21.4.6.3. Manipulyatory, opredelyaemye pol'zovatelem

Kol' skoro s effektivnost'yu potokov vvoda-vyvoda my uzhe razobralis', sleduet pogovorit' ob udobstve. K sozhaleniyu, dlya skol'ko-nibud' slozhnogo formatirovaniya predostavlyaemye potokami sredstva ne prednaznacheny. Ne v tom smysle, chto sredstv net, a v tom, chto oni chrezvychajno neudobny i legko vyvodyat iz sebya privykshego k elegantnomu formatu ...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.1974
Dumayu, 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.


Str.711: 21.6.2. Potoki vvoda i bufera

Funkciya 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);
  1. Dejstviya: Esli !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;
    • esli rdbuf()->in_avail() == 0, ne izvlekaet simvoly
    • esli rdbuf()->in_avail() > 0, izvlekaet min(rdbuf()->in_avail(),n)) simvolov
  2. Vozvrashchaet: Kolichestvo izvlechennyh simvolov.

Str.773: 23.4.3.1. |tap 1: vyyavlenie klassov

Naprimer, v matematike okruzhnost' -- eto chastnyj sluchaj ellipsa, no v bol'shinstve programm okruzhnost' ne nuzhno vyvodit' iz ellipsa, ili delat' ellips potomkom okruzhnosti.

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.


Str.879: A.5. Vyrazheniya

To est' "esli nechto mozhno ponyat' kak ob®yavlenie, eto i est' ob®yavlenie".

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;


Str.931: B.13.2. Druz'ya

Privedennyj v konce stranicy primer nuzhno zamenit' na:
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]

  1. Drugom klassa ili klassa-shablona mozhet byt' funkciya-shablon, klass-shablon, ih specializacii ili obychnaya (ne shablon) funkciya ili klass. Dlya ob®yavleniya funkcij-druzej kotorye ne yavlyayutsya ob®yavleniyami shablonov:
    • esli imya druga yavlyaetsya kvalificirovannym ili nekvalificirovannym template-id, to ob®yavlenie druga ssylaetsya na specializaciyu funkcii-shablona, inache
    • esli imya druga yavlyaetsya qualified-id i v ukazannom klasse ili prostranstve imen najdena sootvetstvuyushchaya funkciya ne shablon, to ob®yavlenie druga ssylaetsya na etu funkciyu, inache
    • esli imya druga yavlyaetsya qualified-id i v ukazannom klasse ili prostranstve imen najdena sootvetstvuyushchaya specializaciya funkcii-shablona, to ob®yavlenie druga ssylaetsya na etu funkciyu, inache
    • imya dolzhno byt' unqualified-id, kotoryj ob®yavlyaet (ili pereob®yavlyaet) obychnuyu (ne shablon) funkciyu.
    Naprimer:
    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.

Str.935: B.13.6. template kak kvalifikator

I snova ob etom zagadochnom kvalifikatore.

V 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:
  1. 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...
  2. get_new2 -- eto staticheskaya funkciya-chlen, pri vyzove iz f, ee imya dolzhno byt' predvareno vse tem zhe kvalifikatorom template po tem zhe prichinam.
  3. A vot 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.
Kak vidite, v sluchae funkcii-druga ya byl vynuzhden ispol'zovat' ne sovsem krasivyj tryuk, t.k. C++ ne pozvolyaet mne pryamo vyrazit' to, chto ya hotel, a imenno: napisat'
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++.

Optimizaciya

Pogovorim ob optimizacii.

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:

  1. Programma dolzhna delat' to, chto ot nee trebuetsya.
  2. Ona dolzhna eto delat' horosho.
Imenno tak: glupo optimizirovat' nepravil'no rabotayushchij kod. Esli zhe pol'zovatelya ustraivaet tekushchee bystrodejstvie -- ne stoit iskat' nepriyatnosti.

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:

  1. rabota s vneshnimi ustrojstvami
  2. sistemnye vyzovy
  3. vyzovy sobstvennyh funkcij
  4. lokal'nye upravlyayushchie struktury
  5. special'nyj podbor komand i optimal'noe ispol'zovanie registrov
Naprimer, ne stoit zanimat'sya voprosami razmeshcheniya upravlyayushchej peremennoj cikla v sootvetstvuyushchem registre processora, esli v dannom cikle proishodit obrashchenie k disku. Vyzovy sobstvennyh funkcij sushchestvenno otlichayutsya ot sistemnyh vyzovov tem, chto kogda my obrashchaemsya k sisteme, proishodit pereklyuchenie konteksta potoka (sistemnyj kod imeet bol'she privilegij, obrashchat'sya k nemu mozhno tol'ko cherez special'nye shlyuzy) i obyazatel'naya proverka dostovernosti peredannyh argumentov (naprimer, sistema proveryaet dejstvitel'no li ej peredana korrektnaya stroka putem ee posimvol'nogo skanirovaniya; esli pri etom proizojdet narushenie prav dostupa ili oshibka adresacii, to prilozhenie budet ob etom proinformirovano; tem samym isklyuchaetsya vozmozhnost' sboya vnutri yadra sistemy, kogda neyasno chto delat' i kto vinovat; naibolee veroyatnyj rezul'tat -- blue death screen, system trap i t.d., t.e. nevosstanovimyj sboj samoj sistemy).

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++".


Makrosy

V C++ makrosy ne nuzhny! Do boli znakomoe vyskazyvanie, ne tak li? YA by ego nemnogo utochnil: ne nuzhny, esli vy ne hotite sushchestvenno oblegchit' sebe zhizn'.

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?

  1. Makros kak nad®yazykovoe sredstvo. Horoshij primerom yavlyaetsya prostoj, no udivitel'no poleznyj otladochnyj makros _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.
  2. Informaciya o tekushchem ishodnom fajle i stroke -- ee pol'zu pri otladke trudno pereocenit'. Dlya etogo ya ispol'zuyu special'nyj makros _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>"
  3. Poluchenie znacheniya chislovogo makrosa v vide stroki. Kak pokazyvaet praktika, dannaya vozmozhnost' nahodit sebe primenenie i za predelami podrobnostej realizacii "mnogoetazhnyh" makrosov. Dopustim, chto dlya vzaimodejstviya s SQL-serverom u nas opredelen klass 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) #arg
    Kstati, 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'.
  4. Mnogokratno vstrechayushchiesya chasti koda. Rassmotrim eshche odin primer iz oblasti raboty s SQL-serverom. Predpolozhim, chto nam nuzhno vybrat' dannye iz nekotoroj tablicy. |to mozhno sdelat' v lob:
    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.
  5. Mnogokratno vstrechayushchiesya podobnye chasti koda. Predstavim sebe, chto my pishem prilozhenie dlya bankovskoj sfery i dolzhny vybrat' informaciyu po nekotorym schetam. V Rossii, naprimer, schet sostoit iz mnogih polej, kotorye dlya udobstva raboty sobirayut v special'nuyu strukturu, a v tablice on mozhet byt' predstavlen smezhnymi polyami s odinakovym prefiksom:
    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.
  6. Rassmotrim bolee tonkij primer podobiya. Pust' nam potrebovalos' sozdat' tablicu dlya hraneniya chasto ispol'zuemoj nami struktury dannyh:
    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&).
Nadeyus', chto posle privedennyh vyshe primerov vy po-novomu posmotrite na rol' makrosov v C++.
Copyright © S. Derevyago, 2000-2004

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
Ocenite etot tekst: