C ++ Succinctly C ++ Språkbruk og Idiomer

Introduksjon

Vi har diskutert RAII-idiomet. Noen språkbruk og programmerings-idiomer i C ++ kan virke utenlandske eller meningsløse ved første øyekast, men de har en hensikt. I dette kapitlet vil vi utforske noen av disse merkelige bruksområder og idiomer for å forstå hvor de kom fra og hvorfor de er brukt.

Du vil vanligvis se C ++ øke et heltall ved å bruke syntaksen ++ i stedet for i ++. Årsaken til dette er delvis historisk, delvis nyttig, og delvis en slags hemmelig håndtrykk. En av de vanligste stedene du vil se dette er i en for loop (f.eks., for (int i = 0; i < someNumber; ++i) … ). Hvorfor bruker C ++ programmerere ++ i stedet for jeg ++? La oss vurdere hva disse to operatørene mener.

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

I den forrige koden, når alle tre setningene er ferdige, vil jeg være lik 2. Men hva vil x og y være lik? De vil begge være lik 1. Dette skyldes at pre-increment operatøren i setningen, ++ i, betyr "øke i og gi den nye verdien av jeg som resultatet." Så når du tilordner x sin verdi går jeg fra 0 til 1, og den nye verdien av i, 1, er tilordnet x. Post-increment operatøren i setningen i ++ betyr "øke i og gi den opprinnelige verdien av i som resultatet." Så når du tilordner y sin verdi, går jeg fra 1 til 2, og den opprinnelige verdien av i, 1, er tilordnet til y.

Hvis vi skulle dekomponere denne rekkefølgen trinnvis som skrevet, eliminere pre-inkrement og post-increment operatører og erstatte dem med regelmessig tillegg, ville vi innse at for å utføre oppgaven til y, trenger vi en ekstra variabel å holde den opprinnelige verdien av i. Resultatet ville være noe slikt:

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

Tidlige kompilatorer, faktisk, pleide å gjøre slike ting. Moderne kompilatorer avgjør nå at det ikke er noen observerbare bivirkninger for å tilordne seg y først, slik at monteringskoden de genererer, selv uten optimalisering, typisk vil se ut som samlingsspråksverdien til denne C ++-koden:

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

På noen måter er ++ i syntaksen (spesielt innenfor en for loop) et holdover fra de tidlige dagene av C ++, og til og med C før den. Å vite at andre C ++-programmerere bruker den, bruker det selv, lar andre vite at du i det minste har noen kjennskap til C ++-bruk og stil - det hemmelige håndtrykket. Den nyttige delen er at du kan skrive en enkelt linje med kode, int x = ++ i;, og få det resultatet du ønsker, heller enn å skrive to linjer med kode: i ++; etterfulgt av int x = i;.

Tips: Mens du kan lagre en kodekode her og der med triks, for eksempel å fange pre-increment operatørens resultat, er det generelt best å unngå å kombinere en rekke operasjoner i en enkelt linje. Kompilatoren kommer ikke til å generere bedre kode, siden den bare vil dekomponere den linjen i komponentdelene (det samme som om du hadde skrevet flere linjer). Derfor vil komprimereren generere maskinskode som utfører hver operasjon på en effektiv måte, adlyder operasjonsorden og andre språkbegrensninger. Alt du gjør, er å forvirre andre mennesker som må se på koden din. Du vil også introdusere en perfekt situasjon for feil, enten fordi du misbrukte noe eller fordi noen gjorde en endring uten å forstå koden. Du vil også øke sannsynligheten for at du selv ikke vil forstå koden hvis du kommer tilbake til det seks måneder senere.


Om null - bruk nullptr

I begynnelsen av livet levde C ++ mange ting fra C, inkludert bruken av binær null som representasjon av null-verdi. Dette har skapt utallige feil gjennom årene. Jeg klandrer ikke Kernighan, Ritchie, Stroustrup, eller noen andre for dette; Det er utrolig hvor mye de oppnådde når de opprettet disse språkene gitt datamaskinene som er tilgjengelige på 70- og 80-tallet. Å forsøke å finne ut hvilke ting som vil være problemer når du oppretter et dataspråk er en ekstremt vanskelig oppgave.

Ikke desto mindre innså programmører tidlig at bruk av en bokstavelig 0 i sin kode kunne forårsake forvirring i noen tilfeller. For eksempel forestill deg at du skrev:

 int * p_x = p_d; // Mer kode her ... p_x = 0;

Mente du å sette pekeren til null som skrevet (dvs. p_x = 0;) eller mente du å sette den spisse verdien til 0 (dvs. * p_x = 0;)? Selv med kode av rimelig kompleksitet kan debuggeren ta betydelig tid til å diagnostisere slike feil.

Resultatet av denne oppfatningen var vedtakelsen av NULL preprosessor makroen: #define NULL 0. Dette ville bidra til å redusere feil, hvis du så * p_x = NULL; eller p_x = 0; da antas at du og de andre programmene brukte konsekvent NULL-makroen, ville feilen være lettere å få øye på, fikse, og fikseringen ville være lettere å verifisere.

Men fordi NULL-makroen er en preprosessordefinisjon, ville kompilatoren aldri se noe annet enn 0 på grunn av tekstlig substitusjon; Det kunne ikke advare deg om muligens feilaktig kode. Hvis noen omdefinerte NULL-makroen til en annen verdi, kan alle slags ekstra problemer oppstå. Redefinere NULL er en veldig dårlig ting å gjøre, men noen ganger programmerer gjør dårlige ting.

C ++ 11 har lagt til et nytt søkeord, nullptr, som kan og skal brukes i stedet for 0, NULL, og alt annet når du må tildele en nullverdi til en peker eller se om en peker er null. Det er flere gode grunner til å bruke det.

Nullptr søkeordet er et språk søkeord; det er ikke eliminert av forprosessoren. Siden det går gjennom kompilatoren, kan kompilatoren oppdage feil og generere bruksvarsler som den ikke kunne oppdage eller generere med den bokstavelige 0 eller noen makroer.

Det kan heller ikke omdefineres enten ved et uhell eller med vilje, i motsetning til en makro som NULL. Dette eliminerer alle feilene som makroer kan introdusere.

Til slutt, det gir fremtidig proofing. Å ha binær null som nullverdien var en praktisk beslutning da den ble laget, men det var likevel tilfeldig. Et annet rimelig valg kan ha vært å ha null være den maksimale verdien av et usignert innfødt heltall. Det er positive og negative til en slik verdi, men det er ingenting jeg vet om som ville ha gjort det ubrukelig.

Med nullptr blir det plutselig mulig å endre hva null er for et bestemt driftsmiljø uten å endre noen C ++-kode som har fullstendig vedtatt nullptr. Kompilatoren kan sammenligne med nullptr, eller tildelingen av nullptr til en pekervariabel, og generere hvilken maskinkode målmiljøet krever fra det. Å prøve å gjøre det samme med et binært 0 ville være veldig vanskelig, om ikke umulig. Hvis noen i fremtiden bestemmer seg for å designe en datamaskinarkitektur og operativsystem som legger til en nullflaggbit for alle minneadresser for å betegne null, kan moderne C ++ støtte det på grunn av nullptr.


Merkelige-ser boolske likestillingskontroller

Du vil vanligvis se folk skrive kode som hvis (nullptr == p_a) .... Jeg har ikke fulgt den stilen i prøvene fordi det bare ser feil ut for meg. I de 18 årene jeg har skrevet programmer i C og C + +, har jeg aldri hatt et problem med problemet denne stilen unngår. Likevel har andre mennesker hatt slike problemer. Denne stilen kan muligens være en del av stilreglene du må følge; Derfor er det verdt å diskutere.

Hvis du skrev hvis (p_a = nullptr) ... i stedet for hvis (p_a == nullptr) ..., så vil programmet tildele nullverdien til p_a, og if-setningen vil alltid evaluere til false. C ++, på grunn av sin C-arv, lar deg få et uttrykk som evaluerer til en hvilken som helst integrert type innenfor parentesene i en kontrollerklæring, for eksempel hvis. C # krever at resultatet av et slikt uttrykk er en boolsk verdi. Siden du ikke kan tildele en verdi til noe som nullptr eller til konstante verdier, for eksempel 3 og 0.0F, hvis du setter den R-verdien på venstre side av en likestillingskontroll, vil kompilatoren varsle deg om feilen. Dette skyldes at du vil tilordne en verdi til noe som ikke kan ha en verdi tildelt den.

Av denne grunn har noen utviklere tatt opp å skrive sine likestillingskontroller på denne måten. Den viktige delen er ikke hvilken stil du velger, men at du er klar over at et oppdrag inne i noe som en hvis uttrykket er gyldig i C ++. På den måten kan du se etter slike problemer.

Uansett hva du gjør, gjør ikke med vilje uttrykk som hvis (x = 3) .... Det er veldig dårlig stil, noe som gjør at koden din er vanskeligere å forstå og mer utsatt for å utvikle feil.


kaste() og noexcept (bool uttrykk)

Merk: Når det gjelder Visual Studio 2012 RC, godtar Visual C ++-kompilatoren, men implementerer ikke unntaksspesifikasjoner. Men hvis du inkluderer en kaste () unntaksspesifikasjon, vil kompilatoren trolig optimalisere bort hvilken kode det ellers ville generere for å støtte avvik når et unntak kastes. Programmet ditt kan ikke kjøre riktig hvis et unntak kastes fra en funksjon merket med kaste (). Andre kompilatorer som implementerer kaste spesifikasjoner, forventer at de merkes riktig, så du bør implementere riktige unntaks spesifikasjoner hvis koden din må kompileres med en annen kompilator.

Merk: Unntaksspesifikasjoner som bruker kaste () syntaksen (kalt dynamisk unntaksspesifikasjoner) blir avskrevet som av C ++ 11. Som sådan kan de bli fjernet fra språket i fremtiden. Den ikke-spesifikke spesifikasjonen og operatøren er erstatninger for denne språkfunksjonen, men implementeres ikke i Visual C ++ fra Visual Studio 2012 RC.

C + + -funksjonene kan spesifisere via kastet () unntakspesifikasjon søkeord om du vil kaste unntak eller ikke, og i så fall hvilken type å kaste.

For eksempel, int AddTwoNumbers (int, int) kaste (); erklærer en funksjon som, på grunn av de tomme parenteser, sier at det ikke kaster noen unntak, unntatt de som fanger internt og ikke kastes om igjen. I kontrast, int AddTwoNumbers (int, int) kaste (std :: logic_error); erklærer en funksjon som sier at det kan kaste et unntak av typen std :: logic_error, eller hvilken som helst type avledet av det.

Funksjonsdeklarasjonen int AddTwoNumber (int, int) kaste (...); erklærer at det kan kaste et unntak av enhver type. Denne syntaksen er Microsoft-spesifikk, så du bør unngå det for kode som kan trenge å bli kompilert med noe annet enn Visual C ++-kompilatoren.

Hvis ingen spesifisering vises, for eksempel i int AddTwoNumbers (int, int);, da kan funksjonen kaste noen unntakstype. Det er det samme som å ha kaste (...) Specifier.

C + + 11 la til den nye noexcept-spesifikasjonen og operatøren. Visual C ++ støtter ikke disse som Visual Studio 2012 RC, men vi diskuterer dem kort siden de uten tvil vil bli lagt til i fremtiden.

Den spesifiserende noexcept (false) er ekvivalent av begge deler kaste (...) og av en funksjon uten et kasteespesifikasjonsnummer. For eksempel, int AddTwoNumbers (int, int) noexcept (false); er ekvivalent av begge deler int AddTwoNumber (int, int) kaste (...); og int AddTwoNumbers (int, int);.

Spesifikasjonene noexcept (sann) og noexcept er ekvivalent av kaste(). Med andre ord, de oppgir alle at funksjonen ikke tillater unntak å unnslippe fra den.

Når man overstyrer en virtuell medlemsfunksjon, kan unntaksspesifikasjonen for overstyringsfunksjonen i den avledede klassen ikke spesifisere unntak utover de deklarerte for typen den er tvingende. La oss se på et eksempel.

#inkludere  #inkludere  klasse A offentlig: A (tomrum) kaste (...); virtuelt ~ A (tomrom) kaste (); virtuelt int Legg til (int, int) kaste (std :: overflow_error); virtuell flyte Legg til (float, float) kaste (); virtuell dobbel Legg til (dobbelt, dobbelt) kaste (int); ; klasse B: offentlig A offentlig: B (tomrom); // Fine, siden det ikke er kaste, er det samme som kaste (...). virtuelt ~ B (tomrom) kaste (); // Fine siden det samsvarer med ~ A. // Int Legg til overstyring er bra siden du alltid kan kaste mindre inn // en overstyring enn basen sier at den kan kaste. virtuelt int Legg til (int, int) kaste () overstyring; // Float Legg til overstyring her er ugyldig fordi A-versjonen sier // det vil ikke kaste, men denne overstyringen sier at det kan kaste et // std :: unntak. virtuelt float Legg til (float, float) kaste (std :: unntak) overstyring; // Den dobbelte Legg til overstyring her er ugyldig fordi A-versjonen sier // det kan kaste en int, men denne overstyringen sier at den kan kaste en dobbel, // som A-versjonen ikke angir. virtuell dobbel Legg til (dobbel, dobbel) kaste (dobbel) overstyring; ;

Fordi syntaksen for kaste unntaksspesifikasjon er utdatert, bør du bare bruke den tomme parentesformen av den, kaste (), for å angi at en bestemt funksjon ikke kaster unntak; ellers bare la den av. Hvis du vil fortelle andre om hvilke unntak dine funksjoner kan kaste, bør du vurdere å bruke kommentarer i headerfiler eller i annen dokumentasjon, og sørg for å holde dem oppdatert..

noexcept (bool uttrykk) er også en operatør. Når det brukes som operatør, tar det et uttrykk som vil evaluere til sant hvis det ikke kan kaste unntak, eller falsk hvis det kan kaste et unntak. Merk at resultatet er en enkel evaluering; det kontrollerer for å se om alle funksjoner som er kalt er noexcept (sann), og hvis det er noen kaste setninger i uttrykket. Hvis det finner noen kasteuttalelser, er de som du vet, ikke tilgjengelige, (f.eks., hvis (x% 2 < 0) throw "This computer is broken"; ) det kan likevel vurderes for falsk siden kompilatoren ikke er nødvendig for å gjøre en dypnivåanalyse.


Pimpl (peker til implementering)

Pointer-to-implementation-idiomet er en eldre teknikk som har fått mye oppmerksomhet i C ++. Dette er bra, fordi det er ganske nyttig. Essensen av teknikken er at i hovedtekstfilen definerer du klassens offentlige grensesnitt. Det eneste datadelementet du har er en privat peker til en fordeklarert klasse eller struktur (pakket inn i en std :: unique_ptr for unntakssikker minnehåndtering), som vil fungere som den faktiske implementeringen.

I kildekodefilen definerer du denne implementeringsklassen og alle dets medlemsfunksjoner og medlemsdata. Publikum fungerer fra grensesnittet til implementeringsklassen for funksjonalitet. Resultatet er at når du har avgjort på det offentlige grensesnittet for klassen din, endres ikke topptekstfilen. Kildetekstfiler som inneholder overskriften trenger ikke å bli kompilert på grunn av implementasjonsendringer som ikke påvirker det offentlige grensesnittet.

Når du vil gjøre endringer i implementeringen, er det eneste som må rekompileres, kildekodefilen der implementeringsklassen eksisterer, snarere enn hver kildekodefil som inneholder klassens headerfil.

Her er en enkel prøve.

Eksempel: PimplSample \ Sandwich.h

#pragma en gang #include  klasse SandwichImpl; Klassisk Sandwich public: Sandwich (void); ~ Sandwich (void); void AddIngredient (const wchar_t * ingrediens); void RemoveIngredient (const wchar_t * ingrediens); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privat: std :: unique_ptr m_pImpl; ;

Eksempel: PimplSample \ Sandwich.cpp

#include "Sandwich.h" #include  #inkludere  #inkludere  bruker namespace std; // Vi kan gjøre noen endringer vi ønsker å gjennomføre klassen uten // utløse en kompilere av andre kildefiler som inkluderer Sandwich.h siden // SandwichImpl er bare definert i denne kildefilen. Derfor må kun denne kilden // filen bli kompilert om vi gjør endringer i SandwichImpl. klasse SandwichImpl offentlig: SandwichImpl (); ~ SandwichImpl (); void AddIngredient (const wchar_t * ingrediens); void RemoveIngredient (const wchar_t * ingrediens); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privat: vektor m_ingredients; wstring m_breadType; wstring m_description; ; SandwichImpl :: SandwichImpl ()  SandwichImpl :: ~ SandwichImpl ()  void SandwichImpl :: AddIngredient (const wchar_t * ingrediens) m_ingredients.emplace_back (ingrediens);  void SandwichImpl :: RemoveIngredient (const wchar_t * ingrediens) auto it = find_if (m_ingredients.begin (), m_ingredients.end (), [=] (wstring element) -> bool return (item.compare (ingredient) = = 0);); hvis (det! = m_ingredients.end ()) m_ingredients.erase (det);  ugyldig SandwichImpl :: SetBreadType (const wchar_t * breadType) m_breadType = breadType;  const wchar_t * SandwichImpl :: GetSandwich (void) m_description.clear (); m_description.append (L "A"); for (auto ingrediens: m_ingredients) m_description.append (ingrediens); 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 ""); returner m_description.c_str ();  Sandwich :: Sandwich (tom): m_pImpl (New SandwichImpl ())  Sandwich :: ~ Sandwich (void)  ​​tomt Sandwich :: AddIngredient (const wchar_t * ingrediens) m_pImpl-> AddIngredient (ingrediens);  ugyldig Sandwich :: RemoveIngredient (const wchar_t * ingrediens) m_pImpl-> RemoveIngredient (ingredient);  void Sandwich :: SetBreadType (const wchar_t * breadType) m_pImpl-> SetBreadType (breadType);  const wchar_t * Sandwich :: GetSandwich (void) return m_pImpl-> GetSandwich (); 

Eksempel: PimplSample \ PimplSample.cpp

#inkludere  #inkludere  #include "Sandwich.h" # include "... /pchar.h" ved hjelp av namespace std; int _pmain (int / * argc * /, _pchar * / * argv * / []) Sandwich s; s.AddIngredient (L "Turkey"); s.AddIngredient (L "Cheddar"); s.AddIngredient (L "salat"); s.AddIngredient (L "tomat"); s.AddIngredient (L "Mayo"); s.RemoveIngredient (L "Cheddar"); s.SetBreadType (L "a Roll"); wcout << s.GetSandwich() << endl; return 0; 

Konklusjon

Beste praksis og idiomer er avgjørende for ethvert språk eller en plattform, så besøk denne artikkelen for å virkelig ta inn det vi har dekket her. Neste opp er maler, en språkfunksjon som lar deg gjenbruke koden din.

Denne leksjonen representerer et kapittel fra C ++ Succinctly, en gratis eBok fra teamet på Syncfusion.