; } 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()
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.
String
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.
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).
|to, voobshche govorya, neverno. Pri primenenii mnozhestvennogo nasledovaniya "prosto kosvennogo vyzova" okazyvaetsya nedostatochno. Rassmotrim sleduyushchuyu programmu:
#include <stdio.h> struct B1 { int b1; // nepustaya virtual ~B1() { } }; struct B2 { int b2; // nepustaya virtual void vfun() { } }; struct D : B1, B2 { // mnozhestvennoe nasledovanie ot nepustyh klassov virtual void vfun() { printf("D::vfun(): this=%p\n", this); } }; int main() { D d; D* dptr=&d; printf("dptr\t%p\n", dptr); dptr->vfun(); B2* b2ptr=&d; printf("b2ptr\t%p\n", b2ptr); b2ptr->vfun(); }Na svoej mashine ya poluchil sleduyushchie rezul'taty:
dptr 0x283fee8 D::vfun(): this=0x283fee8 b2ptr 0x283feec D::vfun(): this=0x283fee8T.e. pri vyzove cherez ukazatel' na proizvodnyj klass
dptr
, vnutri D::vfun()
my poluchim this=0x283fee8
. No nesmotrya na to, chto posle preobrazovaniya ishodnogo ukazatelya v ukazatel' na (vtoroj) bazovyj klass b2ptr
, ego znachenie (ochevidno) izmenilos', vnutri D::vfun()
my vse ravno vidim ishodnoe znachenie, chto polnost'yu sootvetstvuet ozhidaniyam D::vfun()
otnositel'no tipa i znacheniya svoego this
.
CHto zhe vse eto oznachaet? A oznachaet eto to, chto esli by vyzov virtual'noj funkcii
struct D : B1, B2 { virtual void vfun(D *const this) // psevdokod { // ... } };cherez ukazatel'
ptr->vfun()
vsegda svodilsya by k vyzovu (*vtbl[index_of_vfun])(ptr)
, to v nashej programme my by poluchili b2ptr==0x283feec==this!=0x283fee8
.
Vopros nomer dva: kak oni eto delayut? Sut' problemy v tom, chto odna i ta zhe zameshchennaya virtual'naya funkciya (D::vfun()
v nashem sluchae) mozhet byt' vyzvana kak cherez ukazatel' na proizvodnyj klass (ptr==0x283fee8
) tak i cherez ukazatel' na odin iz bazovyh klassov (ptr==0x283feec
), ch'i znacheniya ne sovpadayut, v to vremya kak peredannoe znachenie this
dolzhno byt' odnim i tem zhe (this==0x283fee8
) v oboih sluchayah.
K schast'yu, vtbl
soderzhit raznye zapisi dlya kazhdogo iz variantov vyzova, tak chto reshenie, ochevidno, est'. Na praktike, chashche vsego, ispol'zuetsya odin iz sleduyushchih sposobov:
vtbl
dobavlyaetsya dopolnitel'naya kolonka -- vdelta
. Togda v processe vyzova virtual'noj funkcii krome adresa funkcii iz vtbl
izvlekaetsya i del'ta, ch'e znachenie dobavlyaetsya k ptr
:
addr=vtbl[index].vaddr; // izvlekaem adres funkcii vfun delta=vtbl[index].vdelta; // izvlekaem del'tu, zavisyashchuyu ot sposoba vyzova vfun (*addr)(ptr+delta); // vyzyvaem vfunSushchestvennym nedostatkom dannogo sposoba yavlyaetsya zametnoe uvelichenie razmerov
vtbl
i znachitel'nye nakladnye rashody vremeni vypolneniya: delo v tom, chto absolyutnoe bol'shinstvo vyzovov virtual'nyh funkcij ne trebuet korrekcii znacheniya ptr
, tak chto sootvetstvuyushchie im znacheniya vdelta
budut nulevymi. Dostoinstvom -- vozmozhnost' vyzova virtual'noj funkcii iz ANSI C koda, chto vazhno dlya C++ -> C translyatorov.
ptr
(esli eto voobshche nuzhno):
vfun_entry_0: // ... // sobstvenno kod vfun // ... return; vfun_entry_1: ptr+=delta_1; // korrektiruem znachenie ptr goto vfun_entry_0; // i perehodim k telu vfunV etom sluchae
vtbl
soderzhit tol'ko adresa sootvetstvuyushchih tochek vhoda i nikakih naprasnyh vychislenij ne trebuetsya. Specificheskim nedostatkom dannogo sposoba yavlyaetsya nevozmozhnost' ego realizacii sredstvami ANSI C.
Potomu chto strokovyj literal -- eto ob容kt s vnutrennej komponovkoj (internal linkage).
M-da... Opredelenno, ne samoe udachnoe mesto russkogo perevoda. Tem bolee, chto v originale vse predel'no prosto i ponyatno:
Curiously enough, a template constructor is never used to generate a copy constructor, so without the explicitly declared copy constructor, a default copy constructor would have been generated.Kak ni stranno, konstruktor-shablon nikogda ne ispol'zuetsya dlya generacii konstruktora kopirovaniya, t.e. bez yavno opredelennogo konstruktora kopirovaniya budet sgenerirovan konstruktor kopirovaniya po umolchaniyu.
Dalee hochu otmetit', chto postoyanno vstrechayushchuyusya v perevode frazu "konstruktor shablona" sleduet ponimat' kak "konstruktor-shablon".
Esli vy reshili, chto tem samym dolzhna povysit'sya proizvoditel'nost', vvidu togo, chto v tele funkcii otsutstvuyut bloki try/catch
, to dolzhen vas ogorchit' -- oni budut avtomaticheski sgenerirovany kompilyatorom dlya korrektnoj obrabotki raskrutki steka. No vse-taki, kakaya versiya vydeleniya resursov obespechivaet bol'shuyu proizvoditel'nost'? Davajte protestiruem sleduyushchij kod:
#include <stdio.h> #include <stdlib.h> #include <time.h> void ResourceAcquire(); void ResourceRelease(); void Work(); struct RAII { RAII() { ResourceAcquire(); } ~RAII() { ResourceRelease(); } }; void f1() { ResourceAcquire(); try { Work(); } catch (...) { ResourceRelease(); throw; } ResourceRelease(); } void f2() { RAII raii; Work(); } long Var, Count; void ResourceAcquire() { Var++; } void ResourceRelease() { Var--; } void Work() { Var+=2; } int main(int argc, char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1, c2; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1(); c2=clock(); printf("f1(): %ld mln calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2(); c2=clock(); printf("f2(): %ld mln calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }Kak vydumaete, kakaya funkciya rabotaet bystree? A vot i net! V zavisimosti ot kompilyatora bystree rabotaet to
f1()
, to f2()
, a inogda oni rabotayut sovershenno odinakovo iz-za polnoj identichnosti sgenerirovannogo kompilyatorom koda. Vse zavisit ot ispol'zuemyh principov obrabotki isklyuchenij i kachestva optimizatora.
Kak zhe rabotayut isklyucheniya? Esli vkratce, to v raznyh realizaciyah isklyucheniya rabotayut po-raznomu. I vsegda chrezvychajno netrivial'no! Osobenno mnogo slozhnostej voznikaet s OS, ispol'zuyushchimi tak nazyvaemyj Structured Exception Handling i/ili podderzhivayushchimi mnogopotochnost' (multithreading). Fakticheski, s privychnymi nam sovremennymi OS...
Na tekushchij moment v Internet mozhno najti dostatochnoe kolichestvo materiala po realizacii exception handling (EH) v C++ i ne tol'ko, privodit' zdes' kotoryj ne imeet osobogo smysla. Tem ne menee, vliyanie EH na proizvoditel'nost' C++ programm zasluzhivaet otdel'nogo obsuzhdeniya.
Uvy, no staraniyami nedobrosovestnyh "preuvelichitelej dostoinstv" v massy poshel mif o tom, chto obrabotku isklyuchenij mozhno realizovat' voobshche bez nakladnyh rashodov. Na samom dele eto ne tak, t.k. dazhe samyj sovershennyj metod realizacii EH, otslezhivayushchij sozdannye (i, sledovatel'no, podlezhashchie unichtozheniyu) na dannyj moment (pod)ob容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"...
auto_ptr
<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.
new
T.k. privedennye v knige ob座asneniya nemnogo tumanny, vot sootvetstvuyushchaya chast' standarta:
5.3.4. New [expr.new]
typedef
ne mozhet ee soderzhat'.
Srazu zhe voznikaet vopros: v chem prichina etogo neudobnogo ogranicheniya? D-r Straustrup pishet po etomu povodu sleduyushchee:
The reason is the exception spacification is not part of the type; it is a constraint that is checked on assignment and exforced at run time (rather than at compile time). Some people would like it to be part of the type, but it isn't. The reason is to avoid difficulties when updating large systems with parts from different sources. See "The Design and Evolution of C++" for details.Prichina v tom, chto specifikacii isklyuchenij ne yavlyayutsya chast'yu tipa; dannoe ogranichenie proveryaetsya pri prisvaivanii i prinuditel'no obespechivaetsya vo vremya vypolneniya (a ne vo vremya kompilyacii). Nekotorym lyudyam hotelos' by, chtoby specifikacii isklyuchenij byli chast'yu tipa, no eto ne tak. Prichina v tom, chto my hotim izbezhat' trudnostej, voznikayushchih pri vnesenii izmenenij v bol'shie sistemy, sostoyashchie iz otdel'nyh chastej poluchennyh iz raznyh istochnikov. Obratites' k knige "Dizajn i evolyuciya C++" za detalyami.
Po moemu mneniyu, specifikacii vozbuzhdaemyh isklyuchenij -- eto odna iz samyh neudachnyh chastej opredeleniya C++. Istoricheski, neadekvatnost' sushchestvuyushchego mehanizma specifikacii isklyuchenij obuslovlena otsutstviem real'nogo opyta sistematicheskogo primeneniya isklyuchenij v C++ (i voznikayushchih pri etom voprosov exception safety) na moment ih vvedeniya v opredelenie yazyka. K slovu skazat', o slozhnosti problemy govorit i tot fakt, chto v Java, poyavivshemsya zametno pozzhe C++, specifikacii vozbuzhdaemyh isklyuchenij tak zhe realizovany neudachno.
Imeyushchijsya na tekushchij moment opyt svidetel'stvuet o tom, chto kriticheski vazhnoj dlya napisaniya exception safe koda informaciej yavlyaetsya otvet na vopros: Mozhet li funkciya voobshche vozbuzhdat' isklyucheniya? |ta informaciya izvestna uzhe na etape kompilyacii i mozhet byt' proverena bez osobogo truda.
Tak, naprimer, mozhno vvesti klyuchevoe slovo nothrow
:
// klyuchevoe slovo nothrow otsutstvuet: // f() razresheno vozbuzhdat' lyubye isklyucheniya pryamo ili kosvenno void f() { // ... }
// f() zapreshcheno vozbuzhdat' lyubye isklyucheniya pryamo ili kosvenno, // proveryaetsya na etape kompilyacii void f() nothrow { // ... }
void f() { // zdes' mozhno vozbuzhdat' isklyucheniya pryamo ili kosvenno nothrow { // nothrow-blok // kod, nahodyashchijsya v dannom bloke nikakih isklyuchenij vozbuzhdat' // ne dolzhen, proveryaetsya na etape kompilyacii } // zdes' snova mozhno vozbuzhdat' isklyucheniya }
std::bad_exception
opisannym v dannom razdele obrazom. Vot chto ob etom pishet d-r Straustrup:
The standard doesn't support the mapping of exceptions as I describe it in 14.6.3. It specifies mapping tostd::bad_exception
for exceptions thrown explicitly within anunexpected()
function. This makesstd::bad_exception
an ordinary and rather pointless exception. The current wording does not agree with the intent of the proposer of the mechanism (Dmitry Lenkov of HP) and what he thought was voted in. I have raised the issue in the standards committee.Standart ne podderzhivaet otobrazhenie isklyuchenij v tom vide, kak eto bylo mnoj opisano v razdele 14.6.3. On specificiruet otobrazhenie v
std::bad_exception
tol'ko dlya isklyuchenij, yavno vozbuzhdennyh v funkciiunexpected()
. |to lishaetstd::bad_exception
pervonachal'nogo smysla, delaya ego obychnym i sravnitel'no bessmyslennym isklyucheniem. Tekushchaya formulirovka (standarta) ne sovpadaet s pervonachal'no predlozhennoj Dmitriem Lenkovym iz HP. YA vozbudil sootvetstvuyushchee issue v komitete po standartizacii.
Nu i raz uzh stol'ko slov bylo skazano pro formulirovku iz standarta, dumayu, chto stoit ee privesti:
15.5.2 Funkciya unexpected()
[except.unexpected]
void unexpected();srazu zhe posle zaversheniya raskrutki steka (stack unwinding).
unexpected()
ne mozhet vernut' upravlenie, no mozhet (pere)vozbudit' isklyuchenie. Esli ona vozbuzhdaet novoe isklyuchenie, kotoroe razresheno narushennoj do etogo specifikaciej isklyuchenij, to poisk podhodyashchego obrabotchika budet prodolzhen s tochki vyzova sgenerirovavshej neozhidannoe isklyuchenie funkcii. Esli zhe ona vozbudit nedozvolennoe isklyuchenie, to: Esli specifikaciya isklyuchenij ne soderzhit klass std::bad_exception
(18.6.2.1), to budet vyzvana terminate()
, inache (pere)vozbuzhdennoe isklyuchenie budet zameneno na opredelyaemyj realizaciej ob容kt tipa std::bad_exception
i poisk sootvetstvuyushchego obrabotchika budet prodolzhen opisannym vyshe sposobom.
std::bad_exception
, to lyuboe neopisannoe isklyuchenie mozhet byt' zameneno na std::bad_exception
vnutri unexpected()
.
class XX : B { /* ... */ }; // B -- zakrytyj bazovyj klass class YY : B { /* ... */ }; // B -- otkrytaya bazovaya struktura
Na samom dele, v originale bylo tak:
class XX : B { /* ... */ }; // B -- zakrytaya baza struct YY : B { /* ... */ }; // B -- otkrytaya bazaT.e. vne zavisimosti ot togo, yavlyaetsya li baza
B
klassom ili strukturoj, prava dostupa k unasledovannym chlenam opredelyayutsya tipom naslednika: po umolchaniyu, klass zakryvaet dostup k svoim unasledovannym bazam, a struktura -- otkryvaet.
V principe, v etom net nichego neozhidannogo -- dostup po umolchaniyu k obychnym, ne unasledovannym, chlenam zadaetsya temi zhe pravilami.
Tut, konechno, imeet mesto dosadnaya opechatka, chto, kstati skazat', srazu vidno iz privedennogo primera. T.e. chitat' sleduet tak: ... esli on razreshen po nekotoromu iz vozmozhnyh putej.
|to utverzhdenie, voobshche govorya, neverno i ya vam sovetuyu nikogda tak ne postupat'. Sejchas pokazhu pochemu.
Prezhde vsego, stoit otmetit', chto v C++ vy ne smozhete pryamo vyvesti znachenie ukazatelya na chlen:
struct S { int i; void f(); }; void g() { cout<<&S::i; // oshibka: operator<< ne realizovan dlya tipa int S::* cout<<&S::f; // oshibka: operator<< ne realizovan dlya tipa void (S::*)() }|to dovol'no stranno. Andrew Koenig pishet po etomu povodu, chto delo ne v nedosmotre razrabotchikov biblioteki vvoda/vyvoda, a v tom, chto ne sushchestvuet perenosimogo sposoba dlya vyvoda chego-libo soderzhatel'nogo (kstati, ya okazalsya pervym, kto voobshche ob etom sprosil, tak chto problemu opredelenno nel'zya nazvat' zlobodnevnoj). Moe zhe mnenie sostoit v tom, chto kazhdaya iz realizacij vpolne sposobna najti sposob dlya vyvoda bolee-menee soderzhatel'noj informacii, t.k. v dannom sluchae dazhe neideal'noe reshenie -- eto gorazdo luchshe, chem voobshche nichego.
Poetomu dlya illyustracii vnutrennego predstavleniya ukazatelej na chleny ya napisal sleduyushchij primer:
#include <string.h> #include <stdio.h> struct S { int i1; int i2; void f1(); void f2(); virtual void vf1(); virtual void vf2(); }; const int SZ=sizeof(&S::f1); union { unsigned char c[SZ]; int i[SZ/sizeof(int)]; int S::* iptr; void (S::*fptr)(); } hack; void printVal(int s) { if (s%sizeof(int)) for (int i=0; i<s; i++) printf(" %02x", hack.c[i]); else for (int i=0; i<s/sizeof(int); i++) printf(" %0*x", sizeof(int)*2, hack.i[i]); printf("\n"); memset(&hack, 0, sizeof(hack)); } int main() { printf("sizeof(int)=%d sizeof(void*)=%d\n", sizeof(int), sizeof(void*)); hack.iptr=&S::i1; printf("sizeof(&S::i1 )=%2d value=", sizeof(&S::i1)); printVal(sizeof(&S::i1)); hack.iptr=&S::i2; printf("sizeof(&S::i2 )=%2d value=", sizeof(&S::i2)); printVal(sizeof(&S::i2)); hack.fptr=&S::f1; printf("sizeof(&S::f1 )=%2d value=", sizeof(&S::f1)); printVal(sizeof(&S::f1)); hack.fptr=&S::f2; printf("sizeof(&S::f2 )=%2d value=", sizeof(&S::f2)); printVal(sizeof(&S::f2)); hack.fptr=&S::vf1; printf("sizeof(&S::vf1)=%2d value=", sizeof(&S::vf1)); printVal(sizeof(&S::vf1)); hack.fptr=&S::vf2; printf("sizeof(&S::vf2)=%2d value=", sizeof(&S::vf2)); printVal(sizeof(&S::vf2)); } void S::f1() {} void S::f2() {} void S::vf1() {} void S::vf2() {}Sushchestvennymi dlya ponimaniya mestami zdes' yavlyayutsya ob容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= 00401160Prezhde vsego v glaza brosaetsya to, chto nesmotrya na odinakovyj razmer
int
i void*
, kazhdaya iz realizacij postaralas' otlichit'sya v vybore predstavleniya ukazatelej na chleny, osobenno pervaya. CHto zhe my mozhem skazat' eshche?
Ukazateli na funkcii-chleny vo vtorom kompilyatore realizovany neoptimal'no, t.k. inogda oni soderzhat ukazatel' na "obychnuyu" funkciyu (ffff0000 004014e4
), a inogda -- indeks virtual'noj funkcii (00020000 00000008
). V rezul'tate chego, vmesto togo, chtoby srazu proizvesti kosvennyj vyzov funkcii, kompilyator proveryaet starshuyu chast' pervogo int
, i esli tam stoit -1
(ffff
), to on imeet delo s obychnoj funkciej chlenom, inache -- s virtual'noj. Podobnogo roda proverki pri kazhdom vyzove funkcii-chlena cherez ukazatel' vyzyvayut nenuzhnye nakladnye rashody.
Vnimatel'nyj chitatel' dolzhen sprosit': "Horosho, pust' oni vsegda soderzhat obychnyj ukazatel' na funkciyu, no kak togda byt' s ukazatelyami na virtual'nye funkcii? Ved' my ne mozhem ispol'zovat' odin konkretnyj adres, tak kak virtual'nye funkcii prinyato zameshchat' v proizvodnyh klassah." Pravil'no, dorogoj chitatel'! No vyhod est', i on ocheviden: v etom sluchae kompilyator avtomaticheski generiruet promezhutochnuyu funkciyu-zaglushku.
Naprimer, sleduyushchij kod:
struct S { virtual void vf() { /* 1 */ } void f () { /* 2 */ } }; void g(void (S::*fptr)(), S* sptr) { (sptr->*fptr)(); } int main() { S s; g(S::vf, &s); g(S::f , &s); }prevrashchaetsya v psevdokod:
void S_vf(S *const this) { /* 1 */ } void S_f (S *const this) { /* 2 */ } void S_vf_stub(S *const this) { // virtual'nyj vyzov funkcii S::vf() (this->vptr[index_of_vf])(this); } void g(void (*fptr)(S *const), S* sptr) { fptr(sptr); } int main() { S s; g(S_vf_stub, &s); // obratite vnimanie: ne S_vf !!! g(S_f , &s); }A esli by v C++ prisutstvoval otdel'nyj tip "ukazatel' na virtual'nuyu funkciyu-chlen", on byl by predstavlen prostym indeksom virtual'noj funkcii, t.e. fakticheski prostym
size_t
, i generacii funkcij-zaglushek (so vsemi vytekayushchimi poteryami proizvoditel'nosti) bylo by mozhno izbezhat'. Bolee togo, ego, kak i ukazatel' na dannye-chlen, vsegda mozhno bylo by peredavat' v drugoe adresnoe prostranstvo.
p
ukazyvaet na s
bajtov pamyati, vydelennoj Employee::operator new()
Dannoe predpolozhenie ne vpolne korrektno: p
takzhe mozhet yavlyat'sya nulevym ukazatelem, i v etom sluchae opredelyaemyj pol'zovatelem operator delete()
dolzhen korretno sebya vesti, t.e. nichego ne delat'.
Zapomnite: opredelyaya operator delete()
, vy obyazany pravil'no obrabatyvat' udalenie nulevogo ukazatelya! T.o. kod dolzhen vyglyadet' sleduyushchim obrazom:
void Employee::operator delete(void* p, size_t s) { if (!p) return; // ignoriruem nulevoj ukazatel' // polagaem, chto p ukazyvaet na s bajtov pamyati, vydelennoj // Employee::operator new() i osvobozhdaem etu pamyat' // dlya dal'nejshego ispol'zovaniya }Interesno otmetit', chto standartom special'no ogovoreno, chto argument
p
funkcii
template <class T> void std::allocator::deallocate(pointer p, size_type n);ne mozhet byt' nulevym. Bez etogo zamechaniya ispol'zovanie funkcii
Pool::free
v razdele 19.4.2. "Raspredeliteli pamyati, opredelyaemye pol'zovatelem" bylo by nekorrektnym.
Imenno tak. T.e. esli vy ob座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.
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.
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