C ++ Succinct C ++ limbi și idiomuri

Introducere

Am discutat despre idiomul RAII. Unele utilizări de limbi și idiomuri de programare în C ++ ar putea părea străine sau inutile la prima vedere, dar au un scop. În acest capitol, vom explora câteva dintre aceste uzuri și idiomuri ciudate pentru a înțelege de unde provin acestea și de ce sunt folosite.

Veți vedea în mod obișnuit incrementul C ++ un număr întreg folosind sintaxa ++ i în loc de i ++. Motivul pentru aceasta este parțial istoric, parțial util, și parțial un fel de strângere de mână secrete. Unul dintre locurile comune pe care le veți vedea este într-o buclă (de ex., pentru (int i = 0; i < someNumber; ++i) … ). De ce programatorii C ++ folosesc ++ i mai degrabă decât i ++? Să luăm în considerare ce înseamnă acești doi operatori.

 int i = 0; int x = ++ i; int y = i ++;

În codul anterior, când toate cele trei instrucțiuni termină executarea, voi fi egal cu 2. Dar ce va fi x și y egal? Acestea vor fi egale cu 1. Acest lucru se datorează faptului că operatorul pre-incrementare în instrucțiune, ++ i, înseamnă "increment i și să dau noua valoare a lui i ca rezultat." Deci, atunci când atribuim x valoarea sa, i merge de la 0 la 1, iar noua valoare a i, 1, este atribuită la x. Operatorul post-incrementare în instruciunea i ++ înseamnă "increment i și dă valoarea rezultatului inițial al lui i." Deci atunci când atribuim y valoarea lui, i merge de la 1 la 2, iar valoarea inițială a lui i, 1 este atribuită la y.

Dacă am fi descompus acea secvență de instrucțiuni pas-cu-pas așa cum este scris, eliminând operatorii de pre-incrementare și post-incrementare și înlocuindu-i cu adunări regulate, ne-am da seama că pentru a efectua asignarea lui y avem nevoie de o variabilă suplimentară pentru a păstra valoarea inițială a i. Rezultatul ar fi ceva de genul:

 int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int magicTemp = i; i = i + 1; int y = magicTemp;

Componentele timpurii, de fapt, obișnuiau să facă astfel de lucruri. Componentele moderne determină acum că nu există efecte secundare observabile care să le atribuie mai întâi, astfel încât codul de asamblare pe care îl generează, chiar și fără optimizare, va arăta de obicei ca echivalentul limbaj de asamblare al acestui cod C ++:

 int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int y = i; i = i + 1;

În unele moduri, sintaxa ++ i (mai ales într-o buclă pentru) este o reținere din primele zile ale C ++ și chiar C înainte. Știind că alți programatori C ++ o utilizează, folosiți-o singură, permiteți altora să știe că aveți cel puțin o familiaritate cu obiceiurile și stilul C ++ - strângerea de mână secrete. Partea utilă este că puteți scrie o singură linie de cod, int x = ++ i;, și obțineți rezultatul dorit, în loc să scrieți două linii de cod: i ++; urmat de int x = i;.

Bacsis: În timp ce puteți salva o linie de cod aici și acolo cu trucuri, cum ar fi captarea rezultatului operatorului înaintea creșterii, este în general mai bine să evitați combinarea unei mulțimi de operațiuni într-o singură linie. Compilatorul nu va genera un cod mai bun, deoarece va descompune acea linie în părțile sale componente (la fel ca și când ați fi scris mai multe linii). Prin urmare, compilatorul va genera codul mașinii care efectuează fiecare operațiune într-o manieră eficientă, respectând ordinea operațiilor și alte constrângeri lingvistice. Tot ce veți face este să încurcați alte persoane care trebuie să vă uite codul. Veți introduce, de asemenea, o situație perfectă pentru bug-uri, fie pentru că ați folosit ceva greșit, fie pentru că cineva a făcut o schimbare fără a înțelege codul. De asemenea, veți spori probabilitatea ca dvs. să nu înțelegeți codul dacă reveniți la el în șase luni mai târziu.


Cu privire la Nul - Utilizare nullptr

La începutul vieții, C ++ a adoptat multe lucruri de la C, inclusiv utilizarea binarului zero ca reprezentare a unei valori nula. Acest lucru a creat nenumărate buguri de-a lungul anilor. Nu-l învinovățesc pe Kernighan, pe Ritchie, pe Stroustrup sau pe altcineva pentru asta; este uimitor cât de mult au realizat atunci când au creat aceste limbi, dat fiind computerele disponibile în anii '70 și începutul anilor '80. Încercarea de a descoperi ce lucruri vor fi probleme atunci când creezi o limbă de calculator este o sarcină extrem de dificilă.

Cu toate acestea, începutul anului, programatorii au dat seama că utilizarea unui literal 0 în codul lor ar putea produce confuzie în unele cazuri. De exemplu, imaginați-vă că ați scris:

 int * p_x = p_d; // Mai mult cod aici ... p_x = 0;

V-ați gândit să setați indicatorul la zero ca scris (adică p_x = 0;) sau intenționați să setați valoarea indicată la 0 (adică * p_x = 0;)? Chiar și cu un cod de complexitate rezonabilă, debuggerul ar putea dura un timp semnificativ pentru a diagnostica astfel de erori.

Rezultatul acestei realizări a fost adoptarea macro-ului preprocesor NULL: #define NULL 0. Acest lucru ar ajuta la reducerea erorilor, dacă ați văzut * p_x = NULL; sau p_x = 0; atunci, presupunând că tu și ceilalți programatori utilizați în mod consecvent macroul NULL, eroarea ar fi mai ușor de identificat, repara și remedierea ar fi mai ușor de verificat.

Dar deoarece macroul NULL este o definiție de preprocesor, compilatorul nu ar vedea nimic altceva decât 0 din cauza substituției textuale; nu v-ar putea avertiza despre eventualul cod eronat. Dacă cineva redefinește macro NULL la o altă valoare, ar putea rezulta tot felul de probleme suplimentare. Redefinirea NULL-ului este un lucru foarte rău de făcut, dar uneori programatorii fac lucruri rele.

C ++ 11 a adăugat un nou cuvânt cheie, nullptr, care poate și ar trebui să fie folosit în loc de 0, NULL și orice altceva când trebuie să atribuiți o valoare nulă unui pointer sau să verificați dacă un pointer este nul. Există mai multe motive bune pentru ao folosi.

Cuvântul cheie nullptr este un cuvânt cheie; acesta nu este eliminat de către preprocesor. Deoarece trece prin compilator, compilatorul poate detecta erori și poate genera avertismente de utilizare pe care nu le-a putut detecta sau genera cu literal 0 sau orice macrocomenzi.

De asemenea, nu poate fi redefinită nici accidental, nici intenționat, spre deosebire de o macrocomandă, cum ar fi NULL. Acest lucru elimină toate erorile pe care le pot introduce macrocomenzile.

În cele din urmă, acesta oferă o probă viitoare. Având zero binar ca valoare nulă a fost o decizie practică atunci când a fost făcut, dar a fost arbitrar, totuși. O altă alegere rezonabilă ar fi fost să nu fi valoarea maximă a unui întreg indigen nesemnat. Există pozitive și negative pentru o astfel de valoare, dar nu știu nimic despre asta ar fi făcut-o inutilizabilă.

Cu nullptr, devine brusc fezabil să se schimbe ceea ce este nul pentru un anumit mediu de operare fără a se aduce modificări oricărui cod C ++ care a adoptat pe deplin nullptr. Compilatorul poate face o comparație cu nullptr sau asignarea nullptr la o variabilă a indicelui și poate genera orice cod de mașină pe care mediul țintă îl cere de la acesta. Încercarea de a face același lucru cu un binar 0 ar fi foarte dificil, dacă nu imposibil. Dacă în viitor cineva decide să proiecteze o arhitectură a calculatorului și un sistem de operare care adaugă un bit de pavilion nul pentru toate adresele de memorie pentru a desemna nul, C ++ modern ar putea susține că din cauza nullptr.


Controale egale de tip Boolean ciudate

Veți vedea în mod obișnuit că oamenii scriu coduri, cum ar fi dacă (nullptr == p_a) .... Nu am urmat acel stil în eșantioane, deoarece pur și simplu mi se pare greșit pentru mine. În cei 18 ani în care scriu programe în C și C ++, nu am avut niciodată o problemă cu problema pe care acest stil o evită. Cu toate acestea, alți oameni au avut astfel de probleme. Acest stil ar putea face parte din regulile de stil pe care trebuie să le urmați; prin urmare, merită discutată.

Dacă ai scris dacă (p_a = nullptr) ... in loc de dacă (p_a == nullptr) ..., atunci programul dvs. ar atribui valoarea null pentru p_a și instrucțiunea if va fi întotdeauna evaluată la false. C ++, datorită patrimoniului său C, vă permite să aveți o expresie care se evaluează la orice tip integrat în parantezele unei instrucțiuni de control, cum ar fi dacă. C # cere ca rezultatul unei astfel de expresii să fie o valoare booleană. Deoarece nu puteți să atribuiți o valoare unui element de tip nullptr sau unor valori constante, cum ar fi 3 și 0.0F, dacă puneți această valoare R pe partea stângă a unei verificări a egalității, compilatorul vă va avertiza asupra erorii. Acest lucru se datorează faptului că ați atribuit o valoare ceva care nu poate avea o valoare atribuită.

Din acest motiv, unii dezvoltatori au început să scrie scrisorile de verificare a egalității în acest fel. Partea importantă nu este stilul pe care îl alegeți, dar că sunteți conștient de faptul că o atribuire în interiorul unei expresii dacă este valabilă în C ++. Astfel, știi să cauți astfel de probleme.

Indiferent ce faceți, nu scrieți în mod intenționat declarații cum ar fi dacă (x = 3) .... Acesta este un stil foarte prost, ceea ce face codul dvs. mai greu de înțeles și mai predispus la dezvoltarea de bug-uri.


arunca() și noexcept (expresie bool)

Notă: Din Visual Studio 2012 RC, compilatorul Visual C ++ acceptă, dar nu implementează specificațiile excepționale. Cu toate acestea, dacă includeți o specificație de tip aruncare (), compilatorul va optimiza eventual orice cod care altfel ar genera pentru a sprijini dezlegarea atunci când o excepție este aruncată. Este posibil ca programul dvs. să nu funcționeze corect dacă o excepție este aruncată dintr-o funcție marcată cu aruncare (). Alți compilatori care implementează specificațiile pentru aruncări vor aștepta ca acestea să fie marcate corespunzător, așadar ar trebui să implementați specificațiile excepționale corespunzătoare dacă codul dvs. trebuie să fie compilat cu un alt compilator.

Notă: Exemplele de excepție care folosesc sintaxa throw () (denumite specificații dinamice-excepționale) sunt depreciate începând cu C ++ 11. Ca atare, ei pot fi eliminați din limba în viitor. Specificațiile noexcept și operatorul sunt înlocuiri pentru această caracteristică lingvistică, dar nu sunt implementate în Visual C ++ ca din Visual Studio 2012 RC.

Funcțiile C ++ pot specifica prin cuvântul cheie throw () excepția dacă ar trebui sau nu să arunce excepții și, dacă da, ce fel de aruncare.

De exemplu, int AddTwoNumbers (int, int) arunca (); declară o funcție care, datorită parantezelor goale, afirmă că nu aruncă nici o excepție, cu excepția celor pe care le captează intern și nu aruncă. În contrast, int AddTwoNumbers (int, int) arunca (std :: logic_error); declară o funcție care declară că poate arunca o excepție de tip std :: logic_error, sau orice tip derivat din acesta.

Declarația funcției int AddTwoNumber (int, int) arunca (...); declară că poate arunca o excepție de orice tip. Această sintaxă este specifică Microsoft, deci ar trebui să o evitați pentru un cod care ar putea fi necesar să fie compilat cu altceva decât compilatorul Visual C ++.

Dacă nu apare niciun specificator, cum ar fi int AddTwoNumbers (int, int);, atunci funcția poate arunca orice tip de excepție. Este echivalentul obținerii lui arunca (...) specificator.

C ++ 11 a adăugat noua specificație noexcept (expresia bool) și operatorul. Visual C ++ nu le susține ca pe Visual Studio 2012 RC, dar le vom discuta pe scurt, deoarece vor fi adăugate, fără îndoială, în viitor.

Specificatorul noexcept (fals) este echivalentul ambelor arunca (...) și a unei funcții fără specificator de aruncare. De exemplu, int AddTwoNumbers (int, int) noexcept (false); este echivalentul ambelor int AddTwoNumber (int, int) arunca (...); și int AddTwoNumbers (int, int);.

Specificatorii noexcept (true) și nu sunt echivalente cu arunca(). Cu alte cuvinte, toți precizează că funcția nu permite să scape de excepție.

Atunci când se suprascrie o funcție a unui membru virtual, specificația de excepție a funcției override din clasa derivată nu poate specifica excepții dincolo de cele declarate pentru tipul pe care îl suprascrie. Să ne uităm la un exemplu.

#include  #include  clasa A public: aruncare A (void) (...); virtual ~ A (void) aruncare (); virtual int Adăugați (int, int) aruncați (std :: overflow_error); float virtual adăugați (float, float) aruncați (); duble virtuale Adăugați (dublu, dublu) aruncați (int); ; clasa B: public A public: B (void); // Bine, deoarece nu are o aruncare este același ca aruncarea (...). virtual ~ B (void) throw (); // Fine, deoarece se potrivește cu ~ A. / / Add intride este bine, deoarece poți arunca mai puțin în // o suprascriere decât baza spune că poate arunca. virtual int Adăugați (int, int) arunca () suprascrie; // Float Add override aici este nevalid deoarece versiunea A spune // nu va arunca, dar această suprascriere spune că poate arunca o // std :: excepție. float virtual adăugați (float, float) arunca (std :: excepție) suprascrie; // Adăugarea suprascriirii duble aici nu este validă deoarece versiunea A spune că poate arunca un int, dar această suprascriere spune că poate arunca un dublu, // pe care versiunea A nu o specifică. dublu dublu Adăugați (dublu, dublu) aruncați (dublu) suprascrieți; ;

Deoarece sintaxa specificării excepției de aruncare este depreciată, trebuie să utilizați numai formulele paranteze goale ale acesteia, aruncați (), pentru a specifica faptul că o anumită funcție nu aruncă excepții; în caz contrar, lăsați-o. Dacă doriți să permiteți altora să cunoască excepțiile pe care le pot arunca funcțiile dvs., luați în considerare utilizarea comentariilor în fișierele de antet sau în altă documentație, asigurându-vă că le păstrați la curent.

noexcept (expresie bool) este, de asemenea, un operator. Atunci când este folosit ca operator, este nevoie de o expresie care va evalua dacă este adevărată dacă nu poate arunca o excepție, sau dacă nu poate arunca o excepție. Rețineți că rezultatul este o simplă evaluare; verifică dacă toate funcțiile sunt numite noexcept (true), și dacă există expresii de aruncare în expresie. Dacă afișează declarații de aruncare, chiar și cele pe care le cunoașteți sunt inaccesibile (de ex., dacă (x% 2 < 0) throw "This computer is broken"; ) poate fi, totuși, evaluat la fals deoarece compilatorul nu are obligația de a efectua o analiză profundă.


Pimpl (indicatorul pentru implementare)

Idioma pointer-la-implementare este o tehnică mai veche, care a primit o mulțime de atenție în C ++. Acest lucru este bun, pentru că este destul de util. Esența tehnicii este că în fișierul cu antet definiți interfața publică a clasei. Singurul membru de date pe care îl aveți este un pointer privat la o clasă sau o structură declarată anterior (înfășurată într-un std :: unique_ptr pentru o manipulare excepțională a memoriei), care va servi drept implementare reală.

În fișierul cu cod sursă definiți această clasă de implementare și toate funcțiile sale membre și datele membre. Funcțiile publice de la interfața apel în clasa de implementare pentru funcționalitatea acesteia. Rezultatul este că, odată ce v-ați stabilit pe interfața publică pentru clasa dvs., fișierul antet nu se schimbă niciodată. Astfel, fișierele sursă care includ antetul nu vor trebui recompilate din cauza modificărilor de implementare care nu afectează interfața publică.

Ori de câte ori doriți să faceți modificări la implementare, singurul lucru care trebuie recompilat este fișierul cu cod sursă unde există clasa de implementare, mai degrabă decât fiecare fișier cod sursă care include fișierul de antet de clasă.

Iată un exemplu simplu.

Mostră: PimplSample \ Sandwich.h

#pragma o dată #include  clasa SandwichImpl; sandwich de clasă public: sandwich (void); ~ Sandwich (gol); void AddIngredient (const wchar_t * ingredient); void RemoveIngredient (const wchar_t * ingredient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privat: std :: unique_ptr m_pImpl; ;

Mostră: PimplSample \ Sandwich.cpp

# include "Sandwich.h" #include  #include  #include  folosind namespace std; // Putem face orice schimbări pe care le dorim la clasa de implementare fără a // declanșa o recompilare a altor fișiere sursă care includ Sandwich.h deoarece // SandwichImpl este definită doar în acest fișier sursă. Astfel, numai acest fișier sursă // trebuie să fie recompilat dacă vom face modificări la SandwichImpl. clasa SandwichImpl public: SandwichImpl (); ~ SandwichImpl (); void AddIngredient (const wchar_t * ingredient); void RemoveIngredient (const wchar_t * ingredient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privat: vector m_ingredients; wstring m_breadType; wstring m_description; ; SandwichImpl :: SandwichImpl ()  SandwichImpl :: ~ SandwichImpl ()  void SandwichImpl :: AddIngredient (const wchar_t * ingredient) m_ingredients.emplace_back (ingredient);  void SandwichImpl :: RemoveIngredient (const wchar_t * ingredient) auto it = find_if (m_ingredients.begin (), m_ingredients.end (), [=] element wstring -> bool return (item.compare (ingredient) = 0);); dacă (it! = m_ingredients.end ()) m_ingredients.erase (it);  void SandwichImpl :: setBreadType (const wchar_t * breadType) m_breadType = breadType;  const wchar_t * SandwichImpl :: GetSandwich (void) m_description.clear (); m_description.append (L "A"); pentru (ingredient auto: m_ingredients) m_description.append (ingredient); m_description.append (L ",");  m_description.erase (m_description.end () - 2, m_description.end ()); m_description.append (L "on"); m_description.append (m_breadType); m_description.append (L ""); returnați m_description.c_str ();  Sandwich :: Sandwich (void): m_pImpl (nou SandwichImpl ())  Sandwich :: ~ Sandwich (void)  ​​gol Sandwich :: AddIngredient (const wchar_t * ingredient) m_pImpl-> AddIngredient;  void Sandwich :: RemoveIngredient (const wchar_t * ingredient) m_pImpl-> RemoveIngredient (ingredient);  void Sandwich :: SetBreadType (const wchar_t * breadType) m_pImpl-> SetBreadType (breadType);  const wchar_t * Sandwich :: GetSandwich (void) retur m_pImpl-> GetSandwich (); 

Mostră: PimplSample \ PimplSample.cpp

#include  #include  # include "Sandwich.h" #include "... /pchar.h" folosind namespace std; int _pmain (int / * argc * /, _pchar * / * argv * / []) Sandwich s; s.AddIngredient (L "Turcia"); s.AddIngredient (L "cheddar"); s.AddIngredient (L "Salată"); s.AddIngredient (L "tomate"); s.AddIngredient (L "Mayo"); s.RemoveIngredient (L "cheddar"); s.SetBreadType (L "un Roll"); wcout << s.GetSandwich() << endl; return 0; 

Concluzie

Cele mai bune practici și idiomuri sunt esențiale pentru orice limbă sau platformă, așa că revizuiți acest articol pentru a lua într-adevăr ceea ce am acoperit aici. Următoarele sunt șabloanele, o funcție de limbă care vă permite să reutilizați codul.

Această lecție reprezintă un capitol din C ++ Succinctly, o carte electronică gratuită de la echipa de la Syncfusion.
Cod