; } 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座asneniya 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容kta 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容kty, no kriticheski vazhnoj dlya instanciirovaniya shablonov informaciej yavlyayutsya tipy peredannyh ob容ktov. 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容kt 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容kt Add(z), kotoryj zatem i budet peredan v kachestve argumenta. Posle chego, vnutri for_each() dlya kopii etogo ob容kta 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容kt, 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容kt, k kotoromu zatem primenyaetsya operaciya vyzova funkcii.

Str.344: 11.12. Klass String

Obratite vnimanie, chto dlya nekonstantnogo ob容kta, s.operator[](1) oznachaet Cref(s,1).

A vot zdes' hotelos' by popodrobnee. Pochemu v odnom klasse my mozhem ob座avit' const i ne const funkcii-chleny? Kak osushchestvlyaetsya vybor peregruzhennoj funkcii?

Rassmotrim sleduyushchee ob座avlenie:

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座avlenie 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容kte proizvodnogo klassa yavlyayutsya nadmnozhestvom dannyh bazovogo klassa.

Vmeste s tem, dannaya terminologiya sovershenno estestvenna v teoretiko-mnozhestvennom smysle. A imenno: kazhdyj ob容kt proizvodnogo klassa yavlyaetsya ob容ktom 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容kty, 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容ktov 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容kt 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座avlennogo 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容kty 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座avlen 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容kta neopredelen; v bol'shinstve sluchaev ob容kt 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座asneniya nemnogo tumanny, vot sootvetstvuyushchaya chast' standarta:

5.3.4. New [expr.new]

  1. Esli inicializaciya ob容kta zavershaetsya iz-za vozbuzhdeniya isklyucheniya i mozhet byt' najdena podhodyashchaya funkciya osvobozhdeniya pamyati, ona vyzyvaetsya dlya osvobozhdeniya vydelennoj dlya razmeshcheniya ob容kta 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容kt 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容dinenie 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座avili 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容ktov klassa X (vklyuchaya ob容kty 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容ktom 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容kt 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容kt, 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容ktov 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容ktov
      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);

 B