Gå forbi verdi. Sende parametere etter referanse og etter verdi. Standard instillinger

Så la Faktorial(n) være en funksjon for å beregne faktoren til et tall n. Så, gitt at vi "vet" at faktoren 1 er 1, kan vi konstruere følgende kjede:

Faktoriell(4)=Faktisk(3)*4

Factorial(3)=Factory(2)*3

Faktoriell(2)=Faktisk(1)*2

Men hvis vi ikke hadde en terminalbetingelse om at når n=1 Faktorfunksjonen skulle returnere 1, ville en slik teoretisk kjede aldri ha avsluttet, og dette kunne ha vært en Call Stack Overflow-feil - call stack overflow. For å forstå hva en anropsstabel er og hvordan den kan flyte over, la oss se på den rekursive implementeringen av funksjonen vår:

Funksjon factorial(n: Heltall): LongInt;

Hvis n=1 så

Faktoriell:=Faktisk(n-1)*n;

Slutt;

Som vi kan se, for at kjeden skal fungere riktig, før hvert neste funksjonskall til seg selv, er det nødvendig å lagre alle de lokale variablene et sted, slik at når kjeden er reversert, vil resultatet være korrekt (den beregnede verdien av faktoren til n-1 multipliseres med n ). I vårt tilfelle, hver gang faktorialfunksjonen kalles fra seg selv, må alle verdiene til variabelen n lagres. Området der lokale variabler for en funksjon er lagret når de kaller seg selv rekursivt, kalles anropsstakken. Selvfølgelig er denne stabelen ikke uendelig og kan bli oppbrukt hvis rekursive samtaler er konstruert feil. Begrensningen av iterasjonene i vårt eksempel er garantert av det faktum at når n=1 stopper funksjonskallet.

Sende parametere etter verdi og referanse

Inntil nå kunne vi ikke endre verdien i subrutinen faktisk parameter(dvs. parameteren som er spesifisert når du kaller subrutinen), og i noen applikasjonsoppgaver vil dette være praktisk. La oss huske Val-prosedyren, som endrer verdien av to av de faktiske parameterne samtidig: den første er parameteren der den konverterte verdien til strengvariabelen vil bli skrevet, og den andre er kodeparameteren, hvor nummeret på den feilaktige tegnet plasseres i tilfelle feil under typekonvertering. De. det er fortsatt en mekanisme som en subrutine kan endre de faktiske parameterne med. Dette er mulig takket være ulike måter å sende parametere på. La oss se nærmere på disse metodene.

Programmering i Pascal

Sende parametere etter verdi

I hovedsak er det slik vi ga alle parameterne til rutinene våre. Mekanismen er som følger: når en faktisk parameter er spesifisert, kopieres dens verdi til minneområdet der subrutinen er lokalisert, og deretter, etter at funksjonen eller prosedyren har fullført arbeidet, tømmes dette området. Grovt sett, mens en subrutine kjører, er det to kopier av dens parametere: en i omfanget av det anropende programmet, og den andre i omfanget av funksjonen.

Med denne metoden for å sende parametere, tar det mer tid å ringe subrutinen, siden i tillegg til selve samtalen, er det nødvendig å kopiere alle verdiene til alle faktiske parametere. Hvis en stor mengde data sendes til subrutinen (for eksempel en array med et stort antall elementer), kan tiden som kreves for å kopiere dataene til lokalområdet være betydelig, og dette må tas i betraktning ved utvikling av programmer og finne flaskehalser i ytelsen deres.

Med denne overføringsmetoden kan ikke de faktiske parametrene endres av subrutinen, siden endringene kun vil påvirke et isolert lokalområde, som vil bli frigitt etter at funksjonen eller prosedyren er fullført.

Passere parametere ved referanse

Med denne metoden blir ikke verdiene til de faktiske parameterne kopiert inn i subrutinen, men adressene i minnet (lenker til variabler) der de er plassert, overføres. I dette tilfellet endrer subrutinen allerede verdier som ikke er i det lokale omfanget, så alle endringer vil være synlige for anropsprogrammet.

For å indikere at et argument må sendes ved referanse, legges nøkkelordet var til før deklarasjonen:

Prosedyre getTwoRandom(var n1, n2:Heltall; område: Heltall);

n1:=tilfeldig(område);

n2:=tilfeldig(område); slutt ;

var rand1, rand2: Heltall;

Begynne getToTilfeldig(rand1,rand2,10); SkrivLn(rand1); SkrivLn(rand2);

Slutt.

I dette eksemplet sendes referanser til to variabler til getTwoRandom-prosedyren som faktiske parametere: rand1 og rand2. Den tredje faktiske parameteren (10) overføres med verdi. Prosedyren skriver ved hjelp av formelle parametere

Programmeringsmetoder ved bruk av strenger

Formål med laboratoriearbeid : lære metoder i C#-språket, regler for arbeid med tegndata og ListBox-komponenten. Skriv et program for å jobbe med strenger.

Metoder

En metode er et klasseelement som inneholder programkode. Metoden har følgende struktur:

[attributter] [spesifiserer] typenavn ([parametere])

Metode kropp;

Attributter er spesielle instruksjoner til kompilatoren om egenskapene til en metode. Attributter brukes sjelden.

Kvalifiserte er søkeord som tjener forskjellige formål, for eksempel:

· Bestemme tilgjengeligheten av en metode for andre klasser:

o privat– metoden vil kun være tilgjengelig innenfor denne klassen

o beskyttet– Metoden vil også være tilgjengelig for barneklasser

o offentlig– metoden vil være tilgjengelig for enhver annen klasse som har tilgang til denne klassen

Angir tilgjengeligheten av en metode uten å opprette en klasse

· Innstillingstype

Typen bestemmer resultatet som metoden returnerer: dette kan være hvilken som helst type tilgjengelig i C#, samt void nøkkelordet hvis resultatet ikke er nødvendig.

Metodenavnet er identifikatoren som skal brukes til å kalle metoden. De samme kravene gjelder for en identifikator som for variabelnavn: den kan bestå av bokstaver, tall og et understrek, men kan ikke begynne med et tall.

Parametere er en liste over variabler som kan sendes til en metode når den kalles. Hver parameter består av en variabeltype og et navn. Parametre er atskilt med komma.

Brødteksten til en metode er normal programkode, bortsett fra at den ikke kan inneholde definisjoner av andre metoder, klasser, navnerom osv. Hvis en metode må returnere et resultat, må returnøkkelordet være tilstede på slutten med returverdien. . Hvis det ikke er nødvendig å returnere resultater, er det ikke nødvendig å bruke nøkkelordet retur, selv om det er tillatt.

Et eksempel på en metode som evaluerer et uttrykk:

offentlig dobbel Calc(dobbel a, dobbel b, dobbel c)

return Math.Sin(a) * Math.Cos(b);

dobbel k = Math.Tan(a * b);

return k * Math.Exp(c / k);

Metode Overbelastning

C#-språket lar deg lage flere metoder med samme navn, men forskjellige parametere. Kompilatoren vil automatisk velge den mest passende metoden når du bygger programmet. For eksempel kan du skrive to separate metoder for å heve et tall til en potens: en algoritme vil bli brukt for heltall, og en annen vil bli brukt for reelle tall:

///

/// Beregn X i potensen av Y for heltall

///

privat int Pow(int X, int Y)

///

/// Beregn X i potensen av Y for reelle tall

///

privat dobbel Pow (dobbel X, dobbel Y)

return Math.Exp(Y * Math.Log(Math.Abs(X)));

annet hvis (Y == 0)

Denne koden kalles på samme måte, den eneste forskjellen er i parameterne - i det første tilfellet vil kompilatoren kalle Pow-metoden med heltallsparametere, og i det andre - med reelle parametere:

Standard instillinger

C#-språket, som starter med versjon 4.0 (Visual Studio 2010), lar deg angi standardverdier for noen parametere, slik at du kan utelate noen av parameterne når du kaller en metode. For å gjøre dette, når du implementerer metoden, bør de nødvendige parameterne tildeles en verdi direkte i parameterlisten:

privat void GetData(int Number, int Valgfritt = 5 )

Console.WriteLine("Nummer: (0)", Antall);

Console.WriteLine("Valgfritt: (0)", Valgfritt);

I dette tilfellet kan du kalle metoden som følger:

GetData(10, 20);

I det første tilfellet vil den valgfrie parameteren være lik 20, siden den er eksplisitt spesifisert, og i det andre tilfellet vil den være lik 5, fordi den er ikke spesifisert og kompilatoren tar standardverdien.

Standardparametere kan bare settes på høyre side av parameterlisten; for eksempel vil en slik metodesignatur ikke bli akseptert av kompilatoren:

privat void GetData(int Valgfritt = 5 , int nummer)

Når parametere sendes til en metode på normal måte (uten de ekstra ref og ut nøkkelordene), påvirker ikke eventuelle endringer i parameterne i metoden dens verdi i hovedprogrammet. La oss si at vi har følgende metode:

private void Calc(int Number)

Det kan ses at inne i metoden endres Number-variabelen, som ble sendt som en parameter. La oss prøve å kalle metoden:

Console.WriteLine(n);

Tallet 1 vil vises på skjermen, det vil si at til tross for endringen i variabelen i Calc-metoden, er verdien av variabelen i hovedprogrammet ikke endret. Dette skyldes det faktum at når en metode kalles, a kopiere bestått variabel, er det denne variabelen metoden endres. Når metoden avsluttes, går verdien av kopiene tapt. Denne metoden for å sende en parameter kalles passere ved verdi.

For at en metode skal endre en variabel som sendes til den, må den sendes med ref nøkkelordet - den må være både i metodesignaturen og når den kalles:

privat void Calc(ref int Number)

Console.WriteLine(n);

I dette tilfellet vil tallet 10 vises på skjermen: endringen i verdien i metoden påvirket også hovedprogrammet. Denne metoden overføring kalles passerer ved referanse, dvs. Det er ikke lenger en kopi som overføres, men en referanse til en reell variabel i minnet.

Hvis en metode bruker variabler ved referanse bare for å returnere verdier og den ikke bryr seg om hva som var i dem i utgangspunktet, kan du ikke initialisere slike variabler, men gi dem ut med nøkkelordet ut. Kompilatoren forstår at startverdien til variabelen ikke er viktig og klager ikke over mangelen på initialisering:

privat void Calc(out int Number)

int n; // Vi tildeler ingenting!

strengdatatype

C#-språket bruker strengtypen til å lagre strenger. For å erklære (og som regel umiddelbart initialisere) en strengvariabel, kan du skrive følgende kode:

string a = "Tekst";

streng b = "strenger";

Du kan utføre en tilleggsoperasjon på linjer - i dette tilfellet vil teksten til en linje bli lagt til teksten til en annen:

streng c = a + " " + b; // Resultat: Strengtekst

Strengetypen er faktisk et alias for String-klassen, som lar deg utføre en rekke mer komplekse operasjoner på strenger. For eksempel kan IndexOf-metoden søke etter en delstreng i en streng, og delstrengmetoden returnerer en del av strengen med en spesifisert lengde, med start på en spesifisert posisjon:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Resultat: 14 (teller fra 0)

streng b = a.Substring(3, 5); // Resultat: DEFGH

Hvis du trenger å legge til spesialtegn i en streng, kan du gjøre dette ved å bruke escape-sekvenser som starter med en omvendt skråstrek:

ListBox-komponent

Komponent ListBox er en liste hvis elementer er valgt ved hjelp av tastaturet eller musen. Listen over elementer er spesifisert av egenskapen Varer. Gjenstander er et element som har sine egne egenskaper og sine egne metoder. Metoder Legg til, FjernAt Og Sett inn brukes til å legge til, slette og sette inn elementer.

En gjenstand Varer lagrer objektene i listen. Objektet kan være en hvilken som helst klasse - klassedataene konverteres for visning til en strengrepresentasjon ved hjelp av ToString-metoden. I vårt tilfelle vil strenger fungere som objekter. Men siden Items-objektet lagrer objekter cast-to-type-objekt, før du bruker det, må du caste dem tilbake til sin opprinnelige type, i vårt tilfelle streng:

streng a = (streng)listeBox1.Items;

For å bestemme nummeret på det valgte elementet, bruk egenskapen SelectedIndex.

Da jeg begynte å programmere i C++ og intensivt studerte bøker og artikler, kom jeg alltid over det samme rådet: hvis vi trenger å sende et objekt til en funksjon som ikke skal endres i funksjonen, så bør det alltid sendes videre ved referanse til en konstant(PPSK), bortsett fra de tilfellene når vi trenger å passere enten en primitiv type eller en struktur som ligner på dem. Fordi I mer enn 10 år med programmering i C++ har jeg kommet over dette rådet veldig ofte (og jeg har selv gitt det mer enn én gang), det har lenge vært "absorbert" i meg - jeg sender automatisk alle argumenter med henvisning til en konstant . Men tiden går og 7 år har allerede gått siden vi hadde C++11 til rådighet med dens bevegelsessemantikk, i forbindelse med det hører jeg stadig flere stemmer som stiller spørsmål ved det gode gamle dogmet. Mange begynner å hevde at det å gå forbi ved å referere til en konstant hører fortiden til, og nå er det nødvendig passere ved verdi(PPZ). Hva som ligger bak disse samtalene, samt hvilke konklusjoner vi kan trekke av alt dette, ønsker jeg å diskutere i denne artikkelen.

Bokvisdom

For å forstå hvilken regel vi bør følge, foreslår jeg at du går til bøker. Bøker er en utmerket kilde til informasjon som vi ikke er forpliktet til å akseptere, men som absolutt er verdt å lytte til. Og vi starter med historien, med opprinnelsen. Jeg vil ikke finne ut hvem som var den første apologeten for PPSC, jeg vil bare gi som eksempel boken som personlig hadde størst innflytelse på meg i spørsmålet om bruk av PPSC.

Mayers

Ok, her har vi en klasse der alle parametere sendes ved referanse, er det noen problemer med denne klassen? Dessverre er det det, og dette problemet ligger på overflaten. Vi har 2 funksjonelle enheter i klassen vår: den første tar en verdi på objektopprettingsstadiet, og den andre lar deg endre en tidligere angitt verdi. Vi har to enheter, men fire funksjoner. Tenk deg nå at vi ikke kan ha 2 like enheter, men 3, 5, 6, hva da? Da vil vi møte alvorlig kodeoppblåsthet. Derfor, for ikke å lage en masse funksjoner, var det et forslag om å forlate koblinger i parametere helt:

Mal klasse Holder ( offentlig: eksplisitt Holder(T verdi): m_Value(move(value)) ( ) void setValue(T value) (​m_Value = move(value); ) const T& value() const noexcept ( return m_Value; ) privat: T m_Value; );

Den første fordelen som umiddelbart fanger oppmerksomheten er at det er betydelig mindre kode. Det er enda mindre av det enn i den aller første versjonen, på grunn av fjerningen av const og & (selv om de la til move ). Men vi har alltid blitt lært at å gå gjennom referanse er mer produktivt enn å gå forbi verdi! Slik var det før C++11, og slik er det fortsatt, men nå hvis vi ser på denne koden, vil vi se at det ikke er mer kopiering her enn i den første versjonen, forutsatt at T har en flyttekonstruktør. De. PPSC selv var og vil være raskere enn PPZ, men koden bruker på en eller annen måte den beståtte referansen, og ofte blir dette argumentet kopiert.

Dette er imidlertid ikke hele historien. I motsetning til det første alternativet, hvor vi kun har kopiering, legger vi også til bevegelse. Men å flytte er en billig operasjon, ikke sant? Om dette emnet har Mayers-boken vi vurderer også et kapittel ("Punkt 29") som har tittelen: "Anta at flytteoperasjoner ikke er til stede, ikke er billige og ikke brukes." Hovedideen skal være tydelig fra tittelen, men hvis du vil ha detaljer, må du huske å lese den - jeg vil ikke dvele ved den.

Her vil det være hensiktsmessig å gjennomføre en fullstendig komparativ analyse av den første og siste metoden, men jeg vil ikke avvike fra boken, så vi vil utsette analysen for andre avsnitt, og her vil vi fortsette å vurdere Scotts argumenter. Så, bortsett fra det faktum at det tredje alternativet åpenbart er kortere enn det andre, hva ser Scott på som fordelen med PPZ fremfor PPSC i moderne kode?

Han ser det ved at ved passering av en rverdi, dvs. noen kaller slik: Holder holder(streng("meg")); , alternativet med PPSC vil gi oss kopiering, og alternativet med PPZ vil gi oss bevegelse. På den annen side, hvis overføringen er slik: Holder holder(someLvalue); , så taper PPZ definitivt på grunn av det faktum at den vil utføre både kopiering og flytting, mens i versjonen med PPSC vil det bare være én kopiering. De. det viser seg at PPZ, hvis vi vurderer rent effektivitet, er et slags kompromiss mellom mengden kode og "full" (via && ) støtte for bevegelsessemantikk.

Det er derfor Scott formulerte rådene sine så nøye og fremmer det så nøye. Det virket til og med for meg at han tok det opp motvillig, som om han var under press: han kunne ikke la være å plassere diskusjoner om dette temaet i boken, fordi... det ble diskutert ganske bredt, og Scott var alltid en samler av kollektiv erfaring. I tillegg gir han svært få argumenter til forsvar for PPZ, men han gir mange av dem som setter spørsmålstegn ved denne "teknikken". Vi skal se på argumentene hans mot i senere avsnitt, men her skal vi kort gjenta argumentet Scott kommer med til forsvar for PPP (mentalt legger til "hvis objektet støtter bevegelse og det er billig"): lar deg unngå kopiering når du sender et rvalue-uttrykk som et funksjonsargument. Men nok plager Meyers bok, la oss gå videre til en annen bok.

Forresten, hvis noen har lest boken og er overrasket over at jeg ikke inkluderer et alternativ her med det Mayers kalte universelle referanser - nå kjent som videresendingsreferanser - så er dette enkelt forklart. Jeg vurderer bare PPZ og PPSC, fordi... Jeg anser det som dårlig form å introdusere malfunksjoner for metoder som ikke er maler, bare for å støtte referanse av begge typer (rvalue/lvalue). For ikke å nevne det faktum at koden viser seg annerledes (ikke mer konstanthet) og fører med seg andre problemer.

Josattis og selskap

Den siste boken vi skal se på er "C++ Templates", som også er den nyeste av alle bøkene nevnt i denne artikkelen. Den ble utgitt i slutten av 2017 (og 2018 er angitt inne i boken). I motsetning til andre bøker, er denne helt viet til mønstre, og ikke råd (som Mayers) eller C++ generelt, som Stroustrup. Derfor vurderes fordeler/ulemper her ut fra skrivemalers synspunkt.

Et helt kapittel 7 er viet til dette emnet, som har den veltalende tittelen "By value or by reference?". I dette kapittelet beskriver forfatterne ganske kort, men konsist alle overføringsmetoder med alle deres fordeler og ulemper. En analyse av effektiviteten er praktisk talt ikke gitt her, og det tas for gitt at PPSC vil være raskere enn PPZ. Men med alt dette, på slutten av kapittelet anbefaler forfatterne å bruke standard PPP for malfunksjoner. Hvorfor? Fordi ved å bruke en lenke, vises malparametere fullstendig, og uten en lenke "forfaller de", noe som har en gunstig effekt på behandlingen av matriser og strengliteraler. Forfatterne mener at hvis det for en eller annen type PPP viser seg å være ineffektivt, så kan du alltid bruke std::ref og std::cref . Dette er noen råd, for å være ærlig, har du sett mange mennesker som ønsker å bruke funksjonene ovenfor?

Hva anbefaler de angående PPSC? De anbefaler å bruke PPSC når ytelsen er kritisk eller det er andre tungtveiende grunner til ikke å bruke PPP. Selvfølgelig snakker vi bare om boilerplate-kode her, men dette rådet motsier direkte alt programmerere har blitt lært i et tiår. Dette er ikke bare råd om å vurdere PPP som et alternativ - nei, dette er råd for å gjøre PPSC til et alternativ.

Dette avslutter bokturen vår, fordi... Jeg kjenner ingen andre bøker vi bør konsultere om dette problemet. La oss gå videre til et annet medieområde.

Nettverksvisdom

Fordi Vi lever i Internetts tidsalder, så du bør ikke stole på bokvisdom alene. Dessuten skriver mange forfattere som pleide å skrive bøker nå ganske enkelt blogger og har forlatt bøker. En av disse forfatterne er Herb Sutter, som i mai 2013 publiserte en artikkel på bloggen sin "GotW #4 Solution: Class Mechanics", som, selv om den ikke er helt viet til problemet vi dekker, fortsatt berører det.

Så, i den opprinnelige versjonen av artikkelen, gjentok Sutter ganske enkelt den gamle visdommen: "pass parametere med referanse til en konstant", men vi vil ikke lenger se denne versjonen av artikkelen, fordi Artikkelen inneholder det motsatte rådet: " Hvis parameteren vil fortsatt bli kopiert, og send den deretter med verdi." Igjen det beryktede "hvis". Hvorfor endret Sutter artikkelen, og hvordan visste jeg om det? Fra kommentarene. Les kommentarene til artikkelen hans; de er forresten mer interessante og nyttige enn selve artikkelen. Riktignok endret Sutter etter å ha skrevet artikkelen sin mening, og han gir ikke lenger slike råd. Endringen i mening kan bli funnet i hans tale på CppCon i 2014: «Back to the Basics! Essentials of Modern C++ Style". Sørg for å se, vi går videre til neste Internett-lenke.

Og så har vi hovedprogrammeringsressursen i det 21. århundre: StackOverflow. Eller snarere svaret, med antallet positive reaksjoner som oversteg 1700 på tidspunktet for skriving av denne artikkelen. Spørsmålet er: Hva er kopier-og-bytt-formspråket? , og, som tittelen antyder, ikke helt på temaet vi ser på. Men i sitt svar på dette spørsmålet kommer forfatteren også inn på et tema som interesserer oss. Han anbefaler også å bruke PPZ "hvis argumentet vil bli kopiert likevel" (det er på tide å introdusere en forkortelse for dette også, ved Gud). Og generelt virker dette rådet ganske passende, innenfor rammen av svaret hans og operatøren diskutert der, men forfatteren tar seg friheten til å gi slike råd på en bredere måte, og ikke bare i dette spesielle tilfellet. Dessuten går han lenger enn alle tipsene vi tidligere har diskutert og oppfordrer til å gjøre dette selv i C++03-kode! Hva fikk forfatteren til å trekke slike konklusjoner?

Tilsynelatende hentet forfatteren av svaret hovedinspirasjonen fra en artikkel av en annen bokforfatter og deltidsutvikler av Boost.MPL - Dave Abrahams. Artikkelen heter «Want Speed? Gå forbi verdi." , og den ble publisert tilbake i august 2009, dvs. 2 år før adopsjonen av C++11 og innføringen av bevegelsessemantikk. Som i tidligere tilfeller anbefaler jeg at leseren leser artikkelen på egen hånd, men jeg vil gi hovedargumentene (det er faktisk bare ett argument) som Dave gir til fordel for PPZ: du må bruke PPZ , fordi "hopp over kopi"-optimaliseringen fungerer bra med den (kopi-elision), som er fraværende i PPSC. Leser du kommentarene til artikkelen, kan du se at rådene han fremmer ikke er universelle, noe forfatteren selv bekrefter når han svarer på kritikk fra kommentatorer. Artikkelen inneholder imidlertid eksplisitte råd (veiledning) om å bruke PPP hvis argumentet vil bli kopiert likevel. Hvis noen er interessert kan du forresten lese artikkelen «Vil du ha fart? Ikke gå (alltid) forbi verdi.» . Som tittelen skulle tilsi, er denne artikkelen et svar på Daves artikkel, så hvis du leser den første, sørg for å lese denne også!

Dessverre (heldigvis for noen) gir slike artikler og (enda mer) populære svar på populære nettsteder opphav til massiv bruk av tvilsomme teknikker (et trivielt eksempel) rett og slett fordi dette krever mindre skriving, og det gamle dogmet er ikke lenger urokkelig - Du kan alltid henvise til "det populære rådet" hvis du blir presset til veggen. Nå foreslår jeg at du gjør deg kjent med hva ulike ressurser tilbyr oss med anbefalinger for å skrive kode.

Fordi Siden ulike standarder og anbefalinger nå også er lagt ut på nettet, bestemte jeg meg for å klassifisere denne delen som "nettverksvisdom." Så her vil jeg gjerne snakke om to kilder, hvis formål er å gjøre C++-programmerers kode bedre ved å gi dem tips (retningslinjer) om hvordan man skriver akkurat denne koden.

Det første settet med regler som jeg vil vurdere var dråpen som tvang meg til å ta opp denne artikkelen. Dette settet er en del av det klangryddige verktøyet og eksisterer ikke utenfor det. Som alt relatert til clang, er dette verktøyet veldig populært og har allerede mottatt integrasjon med CLion og Resharper C++ (det var slik jeg kom over det). Så, clang-tydy inneholder en modernize-pass-by-value-regel som fungerer på konstruktører som aksepterer argumenter via PPSC. Denne regelen foreslår at vi erstatter PPSC med PPZ. Dessuten, på tidspunktet for skriving av artikkelen, inneholder beskrivelsen av denne regelen en bemerkning om at denne regelen Ha det fungerer bare for konstruktører, men de (hvem er de?) tar gjerne imot hjelp fra de som utvider denne regelen til andre enheter. Der, i beskrivelsen, er det en lenke til Daves artikkel - det er tydelig hvor beina kommer fra.

Til slutt, for å avslutte denne gjennomgangen av andres visdom og autoritative meninger, foreslår jeg at du ser på de offisielle retningslinjene for å skrive C++-kode: C++ Core Guidelines, hvor hovedredaktørene er Herb Sutter og Bjarne Stroustrup (ikke dårlig, ikke sant?). Så disse anbefalingene inneholder følgende regel: "For "in" parametere, send billig kopierte typer etter verdi og andre ved referanse til const", som fullstendig gjentar den gamle visdommen: PPSK overalt og PPP for små objekter. Dette tipset skisserer flere alternativer å vurdere. i tilfelle argumentet passerer trenger optimalisering. Men PPZ er ikke inkludert i listen over alternativer!

Siden jeg ikke har andre kilder som er verdt å merke seg, foreslår jeg å gå videre til en direkte analyse av begge overføringsmetodene.

Analyse

Hele den foregående teksten er skrevet på en måte som er litt uvanlig for meg: Jeg presenterer andres meninger og prøver til og med å ikke uttrykke mine egne (jeg vet at det blir dårlig). Mye på grunn av det faktum at andres meninger, og målet mitt var å lage en kort oversikt over dem, utsatte jeg en detaljert vurdering av visse argumenter som jeg fant hos andre forfattere. I denne delen skal jeg ikke referere til myndigheter og gi meninger, her skal vi se på noen objektive fordeler og ulemper ved PPSC og PPZ, som vil bli krydret med min subjektive oppfatning. Selvfølgelig vil noe av det som ble diskutert tidligere bli gjentatt, men dessverre, dette er strukturen i denne artikkelen.

Har OPS en fordel?

Så før jeg vurderer argumentene for og imot, foreslår jeg å se på hva og i hvilke tilfeller fordelen ved å gå forbi verdi gir oss. La oss si at vi har en klasse som denne:

Klasse CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotValover(And_NotMalValover)(And_NotMaluer byRefer) erAndNotMover; ) ugyldig setRvaluer (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Selv om vi i denne artikkelen bare er interessert i de to første funksjonene, har jeg inkludert fire alternativer bare for å bruke dem som en kontrast.

Accounter-klassen er en enkel klasse som teller hvor mange ganger den har blitt kopiert/flyttet. Og i CopyMover-klassen har vi implementert funksjoner som lar oss vurdere følgende alternativer:

    flytte bestått argument.

    Pass etter verdi, etterfulgt av kopiering bestått argument.

Nå, hvis vi sender en lverdi til hver av disse funksjonene, for eksempel slik:

Regnskapsfører vedRefer; Regnskapsfører etterValuer; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

da får vi følgende resultater:

Den åpenbare vinneren er PPSC, fordi... gir kun ett eksemplar, mens PPZ gir ett eksemplar og ett trekk.

La oss nå prøve å sende en rverdi:

CopyMover copyMover; copyMover.setByRefer(Accounter()); copyMover.setByValuer(Accounter()); copyMover.setByValuerAndNotMover(Accounter()); copyMover.setRvaluer(Accounter());

Vi får følgende:

Det er ingen klar vinner her, fordi... både PPZ og PPSK har en operasjon hver, men på grunn av at PPZ bruker bevegelse, og PPSK bruker kopiering, kan vi gi seieren til PPZ.

Men eksperimentene våre slutter ikke der; la oss legge til følgende funksjoner for å simulere et indirekte anrop (med påfølgende argumentoverføring):

Void setByValuer(Accounter byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer)); ) void setByRefer(konst Accounter& byRefer, CopyMover& copyMover) ( copyMover.setByRefer(byRefer); ) ...

Vi vil bruke dem nøyaktig på samme måte som vi gjorde uten dem, så jeg vil ikke gjenta koden (se i repository om nødvendig). Så for lverdi vil resultatene være slik:

Merk at PPSC øker gapet med PPZ, forblir med en enkelt kopi, mens PPZ allerede har så mange som 3 operasjoner (en bevegelse til)!

Nå passerer vi rverdien og får følgende resultater:

Nå har PPZ 2 bevegelser, og PPSC har fortsatt ett eksemplar. Er det nå mulig å nominere PPZ som vinner? Nei, fordi hvis ett trekk ikke skal være dårligere enn ett eksemplar, kan vi ikke si det samme om 2 trekk. Derfor vil det ikke være noen vinner i dette eksemplet.

De kan protestere mot meg: «Forfatter, du har en forutinntatt mening, og du trekker inn det som er gunstig for deg. Selv 2 trekk vil være billigere enn å kopiere!» Jeg kan ikke være enig i denne uttalelsen Alt i alt, fordi Hvor mye raskere flytting er enn kopiering avhenger av den spesifikke klassen, men vi skal se på "billig" flytting i en egen del.

Her kom vi inn på en interessant ting: vi la til en indirekte samtale, og PPP la til nøyaktig én operasjon i "vekt". Jeg tror at du ikke trenger å ha et diplom fra MSTU for å forstå at jo flere indirekte anrop vi har, jo flere operasjoner vil bli utført når du bruker PPZ, mens for PPSC vil nummeret forbli uendret.

Alt diskutert ovenfor vil neppe være en åpenbaring for noen; vi har kanskje ikke engang utført eksperimenter - alle disse tallene burde være åpenbare for de fleste C++-programmerere ved første øyekast. Riktignok fortjener ett punkt fortsatt en avklaring: hvorfor, når det gjelder rvalue, har ikke PZ en kopi (eller et annet trekk), men bare ett trekk.

Vel, vi tok en titt på forskjellen i overføring mellom PPZ og PPSC ved å observere førstehånds antall kopier og trekk. Selv om det er åpenbart at fordelen med PPZ fremfor PPSC selv i slike enkle eksempler er mildt sagt Ikke Det er klart at jeg fortsatt, litt pretensiøst, trekker følgende konklusjon: hvis vi fortsatt skal kopiere funksjonsargumentet, er det fornuftig å vurdere å overføre argumentet til funksjonen etter verdi. Hvorfor trakk jeg denne konklusjonen? For å gå jevnt videre til neste seksjon.

Hvis vi kopierer...

Så vi kommer til det ordspråklige "hvis". De fleste av argumentene vi møtte ba ikke om universell implementering av PPP i stedet for PPSC; de ba bare om å gjøre det "hvis argumentet blir kopiert uansett." Det er på tide å finne ut hva som er galt med dette argumentet.

Jeg vil starte med en liten beskrivelse av hvordan jeg skriver kode. I det siste har kodeprosessen min blitt mer og mer lik TDD, dvs. å skrive en klassemetode begynner med å skrive en test der denne metoden vises. Følgelig, når jeg begynner å skrive en test, og lager en metode etter å ha skrevet testen, vet jeg fortsatt ikke om jeg vil kopiere argumentet. Selvfølgelig er ikke alle funksjoner opprettet på denne måten; ofte, selv i prosessen med å skrive en test, vet du nøyaktig hva slags implementering det vil være. Men dette skjer ikke alltid!

Noen vil kanskje innvende mot meg at det ikke spiller noen rolle hvordan metoden opprinnelig ble skrevet, vi kan endre hvordan vi passerer argumentet når metoden har tatt form og det er helt klart for oss hva som skjer der (dvs. om vi har kopiering eller ikke ). Jeg er delvis enig i dette - faktisk kan du gjøre det på denne måten, men dette involverer oss i et slags merkelig spill hvor vi må endre grensesnitt bare fordi implementeringen har endret seg. Noe som bringer oss til neste dilemma.

Det viser seg at vi modifiserer (eller til og med planlegger) grensesnittet basert på hvordan det skal implementeres. Jeg ser ikke på meg selv som en ekspert på OOP og andre teoretiske beregninger av programvarearkitektur, men slike handlinger strider klart mot de grunnleggende reglene når implementeringen ikke skal påvirke grensesnittet. Visse implementeringsdetaljer (enten de er funksjoner i språket eller målplattformen) lekker fortsatt gjennom grensesnittet på en eller annen måte, men du bør prøve å redusere, ikke øke, antallet slike ting.

Vel, Gud velsigne ham, la oss gå denne veien og fortsatt endre grensesnittene avhengig av hva vi gjør i implementeringen når det gjelder å kopiere argumentet. La oss si at vi skrev denne metoden:

Void setName(Name name) ( m_Name = move(name); )

og forpliktet endringene våre til depotet. Etter hvert som tiden gikk, fikk programvareproduktet vårt ny funksjonalitet, nye rammeverk ble integrert, og oppgaven oppsto med å informere omverdenen om endringer i klassen vår. De. Vi vil legge til en varslingsmekanisme til metoden vår, la det være noe som ligner på Qt-signaler:

Void setName(Name name) ( m_Name = move(name); emit nameChanged(m_Name); )

Er det et problem med denne koden? Spise. For hvert kall til setName sender vi et signal, så signalet vil bli sendt selv når betydning m_Name er ikke endret. Foruten ytelsesproblemer, kan denne situasjonen føre til en uendelig sløyfe på grunn av at koden som mottar varselet ovenfor på en eller annen måte kommer til å kalle setName . For å unngå alle disse problemene ser slike metoder oftest slik ut:

Void setName (Navn navn) ( if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); )

Vi ble kvitt problemene beskrevet ovenfor, men nå har regelen vår "hvis vi kopierer..." mislyktes - det er ikke lenger ubetinget kopiering av argumentet, nå kopierer vi det bare hvis det endres! Så hva bør vi gjøre nå? Vil du endre grensesnittet? Ok, la oss endre klassegrensesnittet på grunn av denne løsningen. Hva om klassen vår arvet denne metoden fra et abstrakt grensesnitt? La oss endre det der også! Er det mange endringer fordi implementeringen har endret seg?

Igjen kan de protestere mot meg, sier de, forfatteren, hvorfor prøver du å spare penger på fyrstikker når denne tilstanden vil fungere der ute? Ja, de fleste samtalene vil være falske! Er det noen tillit til dette? Hvor? Og hvis jeg bestemte meg for å spare på kamper, var ikke det faktum at vi brukte PPZ en konsekvens av nettopp slike besparelser? Jeg fortsetter bare "partilinjen" som tar til orde for effektivitet.

Konstruktører

La oss kort gå gjennom konstruktører, spesielt siden det er en spesiell regel for dem i clang-tidy, som ennå ikke fungerer for andre metoder/funksjoner. La oss si at vi har en klasse som denne:

Klasse JustClass ( offentlig: JustClass(const string& justString): m_JustString(justString) ( ) privat: string m_JustString; );

Selvfølgelig er parameteren kopiert, og clang-tidy vil fortelle oss at det ville være en god idé å omskrive konstruktøren til dette:

JustClass(string justString): m_JustString(move(justString)) ( )

Og ærlig talt, det er vanskelig for meg å argumentere her - tross alt kopierer vi egentlig alltid. Og oftest, når vi sender noe gjennom en konstruktør, kopierer vi det. Men oftere betyr ikke alltid. Her er et annet eksempel:

Klasse TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Her kopierer vi ikke alltid, men kun når datoene er korrekt presentert. I de aller fleste tilfeller vil dette selvsagt være tilfelle. Men ikke alltid.

Du kan gi et annet eksempel, men denne gangen uten kode. Tenk deg at du har en klasse som godtar et stort objekt. Klassen har eksistert lenge, og nå er det på tide å oppdatere implementeringen. Vi innser at vi ikke trenger mer enn halvparten av et stort anlegg (som har vokst med årene), og kanskje enda mindre. Kan vi gjøre noe med dette ved å gå forbi verdi? Nei, vi kan ikke gjøre noe, fordi en kopi vil fortsatt bli opprettet. Men hvis vi brukte PPSC, ville vi ganske enkelt endret det vi gjør innsiden designer. Og dette er nøkkelpunktet: ved å bruke PPSC kontrollerer vi hva og når som skjer i implementeringen av funksjonen vår (konstruktør), men hvis vi bruker PPZ, mister vi all kontroll over kopiering.

Hva kan du ta med deg fra denne delen? Det faktum at argumentet «hvis vi kopierer likevel...» er svært kontroversielt, fordi Vi vet ikke alltid hva vi vil kopiere, og selv når vi vet, er vi ofte ikke sikre på at dette vil fortsette i fremtiden.

Flytting er billig

Fra det øyeblikket bevegelsens semantikk dukket opp, begynte den å ha en alvorlig innflytelse på måten moderne C++-kode skrives på, og over tid har denne påvirkningen bare blitt intensivert: det er ikke rart, fordi bevegelse er så billig sammenlignet med kopiering. Men er det det? Er det sant at bevegelse er Alltid billig operasjon? Det er dette vi skal prøve å finne ut i denne delen.

Binært stort objekt

La oss starte med et trivielt eksempel, la oss si at vi har følgende klasse:

Struct Blob ( std::array data; );

Vanlig blob(BDO, engelsk BLOB), som kan brukes i en rekke situasjoner. La oss se på hva det vil koste oss å passere etter referanse og verdi. Vår BDO vil bli brukt omtrent slik:

Void Storage::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) void Storage::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

Og vi vil kalle disse funksjonene slik:

Konst Blob blob(); Oppbevaring; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Koden for andre eksempler vil være identisk med denne, bare med forskjellige navn og typer, så jeg vil ikke gi den for de resterende eksemplene - alt er i depotet.

Før vi går videre til målinger, la oss prøve å forutsi resultatet. Så vi har en 4 KB std::array som vi ønsker å lagre i et Storage-klasseobjekt. Som vi fant ut tidligere, for PPSC vil vi ha en kopi, mens for PPZ vil vi ha en kopi og ett trekk. Basert på det faktum at det er umulig å flytte matrisen, vil det være 2 kopier for PPZ, mot en for PPSC. De. vi kan forvente en dobbelt overlegenhet i ytelse for PPSC.

La oss nå ta en titt på testresultatene:

Denne og alle påfølgende tester ble kjørt på samme maskin ved bruk av MSVS 2017 (15.7.2) og /O2-flagget.

Praksis falt sammen med antagelsen - å passere etter verdi er 2 ganger dyrere, fordi for en matrise er flytting helt ekvivalent med kopiering.

Linje

La oss se på et annet eksempel, en vanlig std::string . Hva kan vi forvente? Vi vet (jeg diskuterte dette i artikkelen) at moderne implementeringer skiller mellom to typer strenger: kort (rundt 16 tegn) og lang (de som er lengre enn korte). For korte brukes en intern buffer, som er en vanlig C-array av røye, men lange vil allerede være plassert på haugen. Vi er ikke interessert i korte linjer, fordi... resultatet der blir det samme som med BDO, så la oss fokusere på lange linjer.

Så, med en lang streng, er det åpenbart at flytting av den skal være ganske billig (bare flytt pekeren), så du kan stole på at flytting av strengen ikke skal påvirke resultatene i det hele tatt, og PPZ bør gi et resultat ikke verre enn PPSC. La oss sjekke det i praksis og få følgende resultater:

Vi vil gå videre til å forklare dette "fenomenet". Så hva skjer når vi kopierer en eksisterende streng til en allerede eksisterende streng? La oss se på et trivielt eksempel:

String først(64, "C"); streng sekund(64, "N"); //... andre = første;

Vi har to strenger på 64 tegn, så den interne bufferen er utilstrekkelig når du oppretter dem, noe som resulterer i at begge strengene blir allokert på heapen. Nå kopierer vi først til andre. Fordi radstørrelsene våre er de samme, åpenbart er det nok plass tildelt i andre til å romme alle dataene fra første, så andre = første; vil være en banal memcpy, ikke noe mer. Men hvis vi ser på et litt modifisert eksempel:

String først(64, "C"); streng andre = første;

da vil det ikke lenger være et kall til operatør= , men kopikonstruktøren blir kalt. Fordi Siden vi har å gjøre med en konstruktør, er det ikke noe eksisterende minne i den. Den må først velges og først deretter kopieres først. De. dette er minnetildeling og deretter memcpy. Som du og jeg vet, er det vanligvis en kostbar operasjon å allokere minne på den globale haugen, så kopiering fra det andre eksemplet vil være dyrere enn å kopiere fra det første. Dyrere minnetildeling per haug.

Hva har dette med temaet vårt å gjøre? Den mest direkte, fordi det første eksemplet viser nøyaktig hva som skjer med PPSC, og det andre viser hva som skjer med PPZ: for PPZ opprettes alltid en ny rad, mens for PPSC gjenbrukes den eksisterende. Du har allerede sett forskjellen i utførelsestid, så det er ingenting å legge til her.

Også her står vi overfor det faktum at ved bruk av OPS jobber vi utenfor konteksten, så vi kan ikke bruke alle fordelene det kan gi. Og hvis vi tidligere har resonnert i form av teoretiske fremtidige endringer, ser vi her en veldig konkret svikt i produktiviteten.

Selvfølgelig kan noen protestere mot meg at strengen skiller seg fra hverandre, og de fleste typer fungerer ikke på den måten. Som jeg kan svare på følgende: alt beskrevet tidligere vil være sant for enhver beholder som tildeler minne i haugen umiddelbart for en pakke med elementer. Også, hvem vet hvilke andre kontekstsensitive optimaliseringer som brukes i andre typer?

Hva bør du ta med deg fra denne delen? At selv om flytting er veldig billig betyr ikke at å erstatte kopiering med copy+moving alltid vil gi et resultat som er sammenlignbart med tanke på ytelse.

Kompleks type

Til slutt, la oss se på en type som vil bestå av flere objekter. La dette være Person-klassen, som består av data som er iboende i en person. Vanligvis er dette ditt fornavn, etternavn, postnummer osv. Du kan representere alt dette som strenger, og anta at strengene du legger inn i feltene til Person-klassen sannsynligvis er korte. Selv om jeg tror at i det virkelige liv vil det være mest nyttig å måle korte strenger, men vi vil fortsatt se på strenger i forskjellige størrelser for å gi et mer fullstendig bilde.

Jeg vil også bruke Person med 10 felter, men for dette vil jeg ikke lage 10 felt direkte i klassekroppen. Implementeringen av Person skjuler en beholder i dypet - dette gjør det mer praktisk å endre testparametere, praktisk talt uten å avvike fra hvordan det ville fungert hvis Person var en ekte klasse. Implementeringen er imidlertid tilgjengelig, og du kan alltid sjekke koden og fortelle meg om jeg har gjort noe galt.

Så, la oss gå: Person med 10 felt av typen streng , som vi overfører ved hjelp av PPSC og PPZ til lagring:

Som du kan se, har vi en enorm forskjell i ytelse, noe som ikke burde komme som en overraskelse for leserne etter de forrige avsnittene. Jeg tror også at Person-klassen er "ekte" nok til at slike resultater ikke vil bli avfeid som abstrakte.

Forresten, da jeg forberedte denne artikkelen, forberedte jeg et annet eksempel: en klasse som bruker flere std::function-objekter. Ifølge min idé skulle det også vise en fordel i ytelsen til PPSC fremfor PPZ, men det viste seg akkurat det motsatte! Men jeg gir ikke dette eksemplet her, ikke fordi jeg ikke likte resultatene, men fordi jeg ikke hadde tid til å finne ut hvorfor slike resultater ble oppnådd. Likevel er det kode i depotet (Printers), tester - også, hvis noen vil finne ut av det, vil jeg gjerne høre om resultatene av forskningen. Jeg planlegger å gå tilbake til dette eksemplet senere, og hvis ingen publiserer disse resultatene før meg, vil jeg vurdere dem i en egen artikkel.

Resultater

Så vi har sett på de ulike fordelene og ulempene ved å passere etter verdi og passere med referanse til en konstant. Vi så på noen eksempler og så på ytelsen til begge metodene i disse eksemplene. Selvfølgelig kan og er ikke denne artikkelen uttømmende, men etter min mening inneholder den nok informasjon til å ta en uavhengig og informert beslutning om hvilken metode som er best å bruke. Noen kan innvende: "hvorfor bruke én metode, la oss starte fra oppgaven!" Selv om jeg generelt sett er enig i denne oppgaven, er jeg uenig i den i denne situasjonen. Jeg tror at det bare kan være én måte å føre argumenter på et språk, som brukes som standard.

Hva betyr standard? Dette betyr at når jeg skriver en funksjon, tenker jeg ikke på hvordan jeg skal sende argumentet, jeg bruker bare "standard". C++-språket er et ganske komplekst språk som mange unngår. Og etter min mening skyldes kompleksiteten ikke så mye kompleksiteten til språkkonstruksjonene som finnes i språket (en typisk programmerer vil kanskje aldri møte dem), men av det faktum at språket får deg til å tenke mye: har jeg frigjort opp minne, er det dyrt å bruke denne funksjonen her? og så videre.

Mange programmerere (C, C++ og andre) er mistroiske og redde for C++ som begynte å dukke opp etter 2011. Jeg har hørt mye kritikk om at språket blir mer komplekst, at bare "guruer" nå kan skrive i det osv. Personlig mener jeg at det ikke er slik – tvert imot bruker komiteen mye tid på å gjøre språket mer brukervennlig for nybegynnere og slik at programmerere trenger å tenke mindre på funksjonene i språket. Tross alt, hvis vi ikke trenger å slite med språket, har vi tid til å tenke på oppgaven. Disse forenklingene inkluderer smarte pekere, lambda-funksjoner og mye mer som dukket opp på språket. Samtidig benekter jeg ikke det faktum at nå må vi studere mer, men hva er galt med å studere? Eller skjer det ingen endringer på andre populære språk som må læres?

Videre er jeg ikke i tvil om at det vil være snobber som kan si som svar: «Du vil ikke tenke? Så skriv i PHP." Jeg vil ikke engang svare til slike mennesker. Jeg vil bare gi et eksempel fra spillvirkelighet: i den første delen av Starcraft, når en ny arbeider blir opprettet i en bygning, for at han skulle begynne å utvinne mineraler (eller gass), måtte han manuelt sendes dit. Dessuten hadde hver pakke mineraler en grense, da økningen i arbeidere var ubrukelig, og de kunne til og med forstyrre hverandre og forverre produksjonen. Dette ble endret i Starcraft 2: arbeidere begynner automatisk å utvinne mineraler (eller gass), og det indikerer også hvor mange arbeidere som for tiden driver med gruvedrift og hvor mye som er grensen for denne forekomsten. Dette forenklet spillerens interaksjon med basen betydelig, slik at han kunne fokusere på viktigere aspekter av spillet: bygge en base, samle tropper og ødelegge fienden. Det ser ut til at dette bare er en stor innovasjon, men hva startet på Internett! Folk (hvem er de?) begynte å skrike at spillet ble "blir ødelagt" og "de drepte Starcraft." Åpenbart kunne slike meldinger bare komme fra "holdere av hemmelig kunnskap" og "adepter med høy APM" som likte å være i en "elite"-klubb.

Så, for å komme tilbake til emnet vårt, jo mindre jeg trenger å tenke på hvordan jeg skriver kode, jo mer tid har jeg til å tenke på å løse det umiddelbare problemet. Å tenke på hvilken metode jeg skal bruke - PPSC eller PPZ - bringer meg ikke en tøddel nærmere å løse problemet, så jeg nekter rett og slett å tenke på slike ting og velger ett alternativ: å gå gjennom en konstant. Hvorfor? Fordi jeg ikke ser noen fordeler for OPS i generelle tilfeller, og spesielle tilfeller må vurderes separat.

Det er et spesielt tilfelle, det er bare det, etter å ha lagt merke til at PPSC i en eller annen metode viser seg å være en flaskehals, og ved å endre overføringen til PPZ, vil vi få en viktig økning i ytelsen, jeg nøler ikke med å bruke PPZ. Men som standard vil jeg bruke PPSC både i vanlige funksjoner og i konstruktører. Og hvis det er mulig, vil jeg fremme denne spesielle metoden der det er mulig. Hvorfor? Fordi jeg synes praksisen med å fremme PPP er ond på grunn av det faktum at brorparten av programmerere ikke er særlig kunnskapsrike (enten i prinsippet, eller rett og slett ikke har kommet i sving med ting ennå), og de følger rett og slett råd. I tillegg, hvis det er flere motstridende råd, velger de det som er enklere, og dette fører til pessimisme i koden ganske enkelt fordi noen hørte noe et sted. Å ja, denne noen kan også gi en lenke til Abrahams sin artikkel for å bevise at han har rett. Og så sitter du, leser koden og tenker: er det faktum at parameteren sendes av verdi her fordi programmereren som skrev dette kom fra Java, bare leste mange "smarte" artikler, eller er det virkelig behov for en teknisk spesifikasjon?

PPSC er mye lettere å lese: personen kjenner tydeligvis den "gode formen" til C++ og vi går videre - blikket henger ikke igjen. Praksisen med å bruke PPSC har blitt lært opp til C++-programmerere i årevis, hva er grunnen til å forlate det? Dette leder meg til en annen konklusjon: hvis et metodegrensesnitt bruker en PPP, så bør det også være en kommentar hvorfor det er slik. I andre tilfeller må PPSC brukes. Selvfølgelig er det unntakstyper, men jeg nevner dem ikke her bare fordi de er underforstått: string_view , initializer_list , ulike iteratorer, etc. Men dette er unntak, listen over disse kan utvides avhengig av hvilke typer som brukes i prosjektet. Men essensen forblir den samme siden C++98: som standard bruker vi alltid PPCS.

For std::string vil det mest sannsynlig ikke være noen forskjell på små strenger, dette skal vi snakke om senere.

Jeg beklager på forhånd for den pretensiøse kommentaren om å "plassere poeng", men vi må på en eller annen måte lokke deg inn i artikkelen)) For min del vil jeg prøve å sikre at sammendraget fortsatt oppfyller dine forventninger.

Kort hva vi snakker om

Alle vet dette allerede, men i begynnelsen vil jeg minne deg på hvordan metodeparametere kan sendes i 1C. De kan sendes "ved referanse" eller "etter verdi". I det første tilfellet gir vi metoden samme verdi som ved anropspunktet, og i det andre en kopi av den.

Som standard, i 1C, sendes argumenter ved referanse, og endringer i en parameter inne i en metode vil være synlig fra utenfor metoden. Her avhenger videre forståelse av spørsmålet av nøyaktig hva du forstår med ordet "endring av parameter". Så dette betyr omdisponering og ingenting mer. Dessuten kan tilordningen være implisitt, for eksempel å kalle en plattformmetode som returnerer noe i utdataparameteren.

Men hvis vi ikke vil at parameteren vår skal sendes ved referanse, kan vi spesifisere et nøkkelord før parameteren Betydning

Prosedyre ByVerdi(Verdiparameter) Parameter = 2; EndProcedure Parameter = 1; ByVerdi(Parameter); Rapport(parameter); // vil skrive ut 1

Alt fungerer som lovet - å endre (eller rettere sagt "erstatte") parameterverdien endrer ikke verdien utenfor metoden.

Vel, hva er vitsen?

Interessante øyeblikk begynner når vi begynner å sende ikke primitive typer (strenger, tall, datoer, etc.) som parametere, men objekter. Det er her begreper som "grunne" og "dype" kopier av et objekt kommer inn i bildet, så vel som pekere (ikke i C++-termer, men som abstrakte håndtak).

Når vi sender et objekt (for eksempel en verditabell) ved referanse, sender vi selve pekerverdien (et bestemt håndtak), som "holder" objektet i plattformens minne. Når den passeres av verdi, vil plattformen lage en kopi av denne pekeren.

Med andre ord, hvis vi ved å sende et objekt ved referanse i en metode tildeler verdien "Array" til parameteren, vil vi ved anropspunktet motta en matrise. Omtildelingen av verdien som er sendt ved referanse, er synlig fra anropsstedet.

Prosedyre ProcessValue(Parameter) Parameter = New Array; EndProcedure Table = New ValueTable; ProcessValue(Tabell); Report(ValueType(Table)); // vil sende ut en Array

Hvis vi passerer objektet etter verdi, vil ikke verditabellen vår gå tapt ved anropet.

Objektets innhold og tilstand

Når du passerer etter verdi, kopieres ikke hele objektet, men bare pekeren. Objektforekomsten forblir den samme. Det spiller ingen rolle hvordan du passerer objektet, ved referanse eller etter verdi - å tømme verditabellen vil tømme selve tabellen. Denne rengjøringen vil være synlig overalt, fordi... det var bare ett objekt, og det spilte ingen rolle hvor nøyaktig det ble overført til metoden.

Prosedyre ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Tabell.Legg til(); ProcessValue(Tabell); Rapport(Tabell.Quantity()); // vil gi ut 0

Når objekter sendes til metoder, opererer plattformen med pekere (betinget, ikke direkte analoger fra C++). Hvis et objekt sendes ved referanse, kan minnecellen til den virtuelle 1C-maskinen som objektet ligger i, overskrives av et annet objekt. Hvis et objekt sendes med verdi, kopieres pekeren og overskriving av objektet resulterer ikke i overskriving av minneplasseringen med det opprinnelige objektet.

Samtidig enhver endring stat objekt (rengjøring, tilføying av egenskaper osv.) endrer selve objektet, og har ingenting i det hele tatt å gjøre med hvordan og hvor objektet ble overført. Tilstanden til en objektforekomst har endret seg; det kan være en haug med "referanser" og "verdier" til den, men forekomsten er alltid den samme. Ved å sende et objekt til en metode, lager vi ikke en kopi av hele objektet.

Og dette er alltid sant, bortsett fra...

Klient-server-interaksjon

Plattformen implementerer serveranrop veldig transparent. Vi kaller ganske enkelt en metode, og under panseret serialiserer plattformen (gjør om til en streng) alle parameterne til metoden, sender dem til serveren og returnerer deretter utdataparametrene tilbake til klienten, hvor de deserialiseres og lever som hvis de aldri hadde vært på noen server.

Som du vet, er ikke alle plattformobjekter serialiserbare. Det er her begrensningen vokser: ikke alle objekter kan sendes til servermetoden fra klienten. Hvis du passerer et ikke-serialiserbart objekt, vil plattformen begynne å bruke stygge ord.

  • En eksplisitt erklæring om programmererens intensjoner. Ved å se på metodesignaturen kan du tydelig se hvilke parametere som er input og hvilke som er utdata. Denne koden er lettere å lese og vedlikeholde
  • For at en endring i «by reference»-parameteren på serveren skal være synlig ved anropspunktet på klienten, p. Selve plattformen vil nødvendigvis returnere parametrene som sendes til serveren via lenke til klienten for å sikre atferden beskrevet i begynnelsen av artikkelen. Hvis parameteren ikke trenger å returneres, vil det være trafikkoverløp. For å optimalisere datautveksling, bør parametere hvis verdier vi ikke trenger ved utgangen, merkes med ordet Verdi.

Det andre punktet er bemerkelsesverdig her. For å optimalisere trafikken vil ikke plattformen returnere parameterverdien til klienten hvis parameteren er merket med ordet Verdi. Alt dette er flott, men det fører til en interessant effekt.

Som jeg allerede sa, når et objekt overføres til serveren, skjer serialisering, dvs. en "dyp" kopi av objektet utføres. Og hvis det er et ord Betydning objektet vil ikke reise fra serveren tilbake til klienten. Vi legger til disse to fakta og får følgende:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Value Parameter) Parameter.Clear(); EndProcedure &OnClient-prosedyre ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient-prosedyre CheckValue() Liste1= Nye listeverdier; List1.Add("hei"); Liste2 = Liste1.Kopi(); Liste3 = Liste1.Kopi(); // objektet kopieres fullstendig, // overføres til serveren, og returneres deretter. // sletting av listen er synlig ved call point ByRef(List1); // objektet kopieres fullstendig, // overføres til serveren. Det kommer ikke tilbake. // Å tømme listen er IKKE SYNLIG når du kaller ByValue(List2); // bare objektpekeren blir kopiert // sletting av listen er synlig ved punktet av kallet til ByValueClient(List3); Rapport(Liste1.Antal()); Rapport(Liste2.Antall()); Rapport(List3.Quantity()); Slutt på prosedyre

Sammendrag

Kort oppsummert kan det oppsummeres slik:

  • Ved å passere ved referanse kan du "overskrive" et objekt med et helt annet objekt
  • Passering av verdi lar deg ikke "overskrive" objektet, men endringer i den interne tilstanden til objektet vil være synlige, fordi vi jobber med samme objektforekomst
  • Når du foretar et serverkall, jobbes det med ULIKE forekomster av objektet, fordi En dyp kopi ble utført. Nøkkelord Betydning vil forhindre at serverforekomsten kopieres tilbake til klientforekomsten, og endring av den interne tilstanden til et objekt på serveren vil ikke føre til en lignende endring på klienten.

Jeg håper at denne enkle listen med regler vil gjøre det lettere for deg å løse tvister med kolleger angående overføring av parametere "etter verdi" og "ved referanse"