Sergej Derevyago. C++ 3rd: kommentarii --------------------------------------------------------------- © Copyright Sergej Derevyago, 2000 Versiya ispravlennaya i dopolnennaya, 12 Oct 2004 Origin: http://ders.stml.net/cpp/ ¡ 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)