Passera på värde. Skicka parametrar genom referens och värde. Standardinställningar

Så låt Factorial(n) vara en funktion för att beräkna faktorialet för ett tal n. Sedan, givet att vi "vet" att faktor 1 är 1, kan vi konstruera följande kedja:

Faktoriell(4)=Faktoriell(3)*4

Faktoriell(3)=Faktoriell(2)*3

Faktoriell(2)=Faktoriell(1)*2

Men om vi inte hade ett terminalvillkor att när n=1 skulle faktorfunktionen returnera 1, så skulle en sådan teoretisk kedja aldrig ha tagit slut, och detta kunde ha varit ett Call Stack Overflow-fel - call stack overflow. För att förstå vad en anropsstack är och hur den kan svämma över, låt oss titta på den rekursiva implementeringen av vår funktion:

Funktion factorial(n: Heltal): LongInt;

Om n=1 då

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

Slutet;

Som vi ser, för att kedjan ska fungera korrekt, innan varje nästa funktionsanrop till sig själv, är det nödvändigt att spara alla lokala variabler någonstans, så att när kedjan vänds blir resultatet korrekt (det beräknade värdet av faktorial av n-1 multipliceras med n ). I vårt fall, varje gång faktorialfunktionen anropas från sig själv, måste alla värden för variabeln n sparas. Området där lokala variabler för en funktion lagras när den anropar sig själv rekursivt kallas anropsstacken. Naturligtvis är denna stack inte oändlig och kan uttömmas om rekursiva anrop är felaktigt konstruerade. Ändligheten av iterationerna i vårt exempel garanteras av det faktum att när n=1 upphör funktionsanropet.

Skicka parametrar efter värde och referens

Hittills har vi inte kunnat ändra värdet i subrutinen faktisk parameter(dvs parametern som specificeras när subrutinen anropas), och i vissa tillämpade uppgifter skulle detta vara bekvämt. Låt oss komma ihåg Val-proceduren, som ändrar värdet på två av dess faktiska parametrar på en gång: den första är parametern där det konverterade värdet för strängvariabeln kommer att skrivas, och den andra är Code-parametern, där numret på de felaktiga tecken placeras i händelse av fel under typkonvertering. De där. det finns fortfarande en mekanism genom vilken en subrutin kan ändra de faktiska parametrarna. Detta är möjligt tack vare olika sätt att skicka parametrar. Låt oss ta en närmare titt på dessa metoder.

Programmering i Pascal

Överför parametrar efter värde

I huvudsak är det så här vi skickade alla parametrar till våra rutiner. Mekanismen är följande: när en faktisk parameter specificeras, kopieras dess värde till minnesområdet där subrutinen finns och sedan, efter att funktionen eller proceduren har slutfört sitt arbete, rensas detta område. Grovt sett, medan en subrutin körs, finns det två kopior av dess parametrar: en i omfattningen av det anropande programmet och den andra i omfånget av funktionen.

Med denna metod för att skicka parametrar tar det mer tid att anropa subrutinen, eftersom det förutom själva samtalet är nödvändigt att kopiera alla värden för alla faktiska parametrar. Om en stor mängd data skickas till subrutinen (till exempel en array med ett stort antal element) kan tiden som krävs för att kopiera datan till det lokala området vara betydande och detta måste beaktas vid utveckling av program och hitta flaskhalsar i sin prestation.

Med denna överföringsmetod kan de faktiska parametrarna inte ändras av subrutinen, eftersom ändringarna endast kommer att påverka ett isolerat lokalt område, som kommer att släppas efter att funktionen eller proceduren är klar.

Passar parametrar genom referens

Med denna metod kopieras inte värdena för de faktiska parametrarna till subrutinen, utan adresserna i minnet (länkar till variabler) där de finns överförs. I det här fallet ändrar subrutinen redan värden som inte finns i det lokala omfånget, så alla ändringar kommer att vara synliga för det anropande programmet.

För att indikera att ett argument måste skickas genom referens läggs nyckelordet var till före dess deklaration:

Procedur getTwoRandom(var n1, n2:Heltal; intervall: heltal);

n1:=slumpmässig(område);

n2:=slumpmässig(intervall); slutet ;

var rand1, rand2: heltal;

Börja getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Slutet.

I det här exemplet skickas referenser till två variabler till getTwoRandom-proceduren som faktiska parametrar: rand1 och rand2. Den tredje faktiska parametern (10) skickas med värdet. Proceduren skriver med formella parametrar

Programmeringsmetoder med strängar

Syfte med laboratoriearbete : lär dig metoder i C#-språket, regler för att arbeta med teckendata och ListBox-komponenten. Skriv ett program för att arbeta med strängar.

Metoder

En metod är ett klasselement som innehåller programkod. Metoden har följande struktur:

[attribut] [specificerare] typnamn ([parametrar])

Metodkropp;

Attribut är speciella instruktioner till kompilatorn om egenskaperna hos en metod. Attribut används sällan.

Kvalificerade är sökord som tjänar olika syften, till exempel:

· Bestämma tillgängligheten av en metod för andra klasser:

o privat– metoden kommer endast att vara tillgänglig inom denna klass

o skyddad– Metoden kommer även att vara tillgänglig för barnklasser

o offentlig– Metoden kommer att vara tillgänglig för alla andra klasser som kan komma åt den här klassen

Anger tillgängligheten för en metod utan att skapa en klass

· Inställningstyp

Typen bestämmer resultatet som metoden returnerar: detta kan vara vilken typ som helst som är tillgänglig i C#, samt nyckelordet void om resultatet inte krävs.

Metodnamnet är identifieraren som kommer att användas för att anropa metoden. Samma krav gäller för en identifierare som för variabelnamn: den kan bestå av bokstäver, siffror och ett understreck, men kan inte börja med en siffra.

Parametrar är en lista över variabler som kan skickas till en metod när den anropas. Varje parameter består av en variabeltyp och ett namn. Parametrar separeras med kommatecken.

Brödtexten i en metod är normal programkod, förutom att den inte kan innehålla definitioner av andra metoder, klasser, namnrymder etc. Om en metod måste returnera något resultat, måste returnyckelordet finnas i slutet med returvärdet. . Om det inte är nödvändigt att returnera resultat är det inte nödvändigt att använda nyckelordet retur, även om det är tillåtet.

Ett exempel på en metod som utvärderar ett uttryck:

public double Calc(dubbel a, dubbel b, dubbel c)

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

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

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

Metod Överbelastning

C#-språket låter dig skapa flera metoder med samma namn men olika parametrar. Kompilatorn väljer automatiskt den mest lämpliga metoden när du bygger programmet. Till exempel kan du skriva två separata metoder för att höja ett tal till en potens: en algoritm skulle användas för heltal och en annan skulle användas för reella tal:

///

/// Beräkna X i potensen av Y för heltal

///

privat int Pow(int X, int Y)

///

/// Beräkna X i potensen av Y för reella tal

///

privat dubbel Pow (dubbel X, dubbel Y)

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

annars om (Y == 0)

Den här koden anropas på samma sätt, den enda skillnaden är i parametrarna - i det första fallet kommer kompilatorn att anropa Pow-metoden med heltalsparametrar, och i det andra - med verkliga parametrar:

Standardinställningar

C#-språket, som börjar med version 4.0 (Visual Studio 2010), låter dig ställa in standardvärden för vissa parametrar, så att när du anropar en metod kan du utelämna några av parametrarna. För att göra detta, när metoden implementeras, bör de nödvändiga parametrarna tilldelas ett värde direkt i parameterlistan:

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

Console.WriteLine("Number: (0)", Antal);

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

I det här fallet kan du anropa metoden enligt följande:

GetData(10, 20);

I det första fallet kommer parametern Optional att vara lika med 20, eftersom den är explicit specificerad, och i det andra fallet kommer den att vara lika med 5, eftersom det är inte explicit specificerat och kompilatorn tar standardvärdet.

Standardparametrar kan bara ställas in på höger sida av parameterlistan; till exempel kommer en sådan metodsignatur inte att accepteras av kompilatorn:

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

När parametrar skickas till en metod på vanligt sätt (utan de extra nyckelorden ref och ut), påverkar inte alla ändringar av parametrarna i metoden dess värde i huvudprogrammet. Låt oss säga att vi har följande metod:

privat void Calc(int Number)

Det kan ses att inuti metoden ändras Number-variabeln, som skickades som en parameter. Låt oss försöka kalla metoden:

Console.WriteLine(n);

Siffran 1 kommer att visas på skärmen, det vill säga trots förändringen i variabeln i Calc-metoden har värdet på variabeln i huvudprogrammet inte ändrats. Detta beror på det faktum att när en metod anropas, a kopiera passerad variabel är det denna variabel som metoden ändras. När metoden avslutas försvinner värdet på kopiorna. Denna metod för att skicka en parameter kallas passera värdet.

För att en metod ska kunna ändra en variabel som skickas till den måste den skickas med nyckelordet ref - den måste finnas både i metodsignaturen och när den anropas:

privat void Calc(ref int Number)

Console.WriteLine(n);

I det här fallet kommer siffran 10 att visas på skärmen: förändringen i värdet i metoden påverkade också huvudprogrammet. Denna metod överföring kallas passerar genom referens, dvs. Det är inte längre en kopia som överförs, utan en referens till en verklig variabel i minnet.

Om en metod använder variabler genom referens endast för att returnera värden och den inte bryr sig om vad som fanns i dem från början, kan du inte initiera sådana variabler, utan skicka dem med nyckelordet out. Kompilatorn förstår att variabelns initiala värde inte är viktigt och klagar inte på bristen på initialisering:

privat void Calc(out int Number)

int n; // Vi tilldelar ingenting!

strängdatatyp

C#-språket använder strängtypen för att lagra strängar. För att deklarera (och som regel omedelbart initiera) en strängvariabel kan du skriva följande kod:

string a = "Text";

sträng b = "strängar";

Du kan utföra en tilläggsoperation på rader - i det här fallet kommer texten från en rad att läggas till texten på en annan:

sträng c = a + " " + b; // Resultat: Strängtext

Strängtypen är faktiskt ett alias för String-klassen, vilket låter dig utföra ett antal mer komplexa operationer på strängar. Till exempel kan metoden IndexOf söka efter en delsträng i en sträng, och metoden delsträng returnerar en del av strängen med en angiven längd, med början på en angiven position:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Resultat: 14 (räknat från 0)

sträng b = a.Substring(3, 5); // Resultat: DEFGH

Om du behöver lägga till specialtecken i en sträng kan du göra detta med escape-sekvenser som börjar med ett omvänt snedstreck:

ListBox-komponent

Komponent ListBoxär en lista vars element väljs med tangentbordet eller musen. Listan över element anges av egenskapen Föremål. Föremål är ett element som har sina egna egenskaper och sina egna metoder. Metoder Lägg till, Ta bortAt Och Föra in används för att lägga till, ta bort och infoga element.

Ett objekt Föremål lagrar objekten i listan. Objektet kan vara vilken klass som helst - klassdata konverteras för visning till en strängrepresentation med ToString-metoden. I vårt fall kommer strängar att fungera som objekt. Men eftersom objektet Items lagrar objekt cast-to-typ-objekt, innan du använder det måste du casta tillbaka dem till sin ursprungliga typ, i vårt fall sträng:

sträng a = (sträng)listBox1.Items;

För att bestämma numret på det valda elementet, använd egenskapen SelectedIndex.

När jag började programmera i C++ och intensivt studerade böcker och artiklar, stötte jag alltid på samma råd: om vi behöver skicka något objekt till en funktion som inte ska ändras i funktionen, så ska det alltid skickas vidare med hänvisning till en konstant(PPSK), förutom de fall då vi behöver passera antingen en primitiv typ eller en struktur som liknar dem i storlek. Därför att Under mer än 10 års programmering i C++ har jag stött på detta råd väldigt ofta (och jag har själv gett det mer än en gång), det har länge "absorberats" i mig - jag skickar automatiskt alla argument med hänvisning till en konstant . Men tiden går och 7 år har redan gått sedan vi hade C++11 till vårt förfogande med dess rörelsesemantik, i samband med vilken jag hör allt fler röster som ifrågasätter den gamla goda dogmen. Många börjar hävda att det att passera med hänvisning till en konstant är ett minne blott och nu är det nödvändigt passera värdet(PPZ). Vad som ligger bakom dessa samtal, samt vilka slutsatser vi kan dra av allt detta vill jag diskutera i den här artikeln.

Bokvisdom

För att förstå vilken regel vi bör följa föreslår jag att vi vänder oss till böcker. Böcker är en utmärkt källa till information som vi inte är skyldiga att acceptera, men som verkligen är värd att lyssna på. Och vi börjar med historien, med ursprunget. Jag kommer inte att ta reda på vem som var den första apologeten för PPSC, jag ska helt enkelt ge som exempel den bok som personligen hade störst inflytande på mig i frågan om att använda PPSC.

Mayers

Okej, här har vi en klass där alla parametrar skickas med referens, finns det några problem med denna klass? Tyvärr finns det, och detta problem ligger på ytan. Vi har 2 funktionella entiteter i vår klass: den första tar ett värde vid objektskapandet, och den andra låter dig ändra ett tidigare inställt värde. Vi har två enheter, men fyra funktioner. Föreställ dig nu att vi inte kan ha två liknande enheter, utan 3, 5, 6, vad då? Då kommer vi att möta allvarlig koduppblåsthet. Därför, för att inte skapa en massa funktioner, fanns det ett förslag att överge länkar i parametrar helt och hållet:

Mall class Holder ( public: explicit Holder(T value): 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örsta fördelen som direkt fångar ditt öga är att det finns betydligt mindre kod. Det finns ännu mindre av det än i den allra första versionen, på grund av borttagningen av const och & (även om de lade till move ). Men vi har alltid fått lära oss att det är mer produktivt att passera genom referens än att passera på värde! Så här var det innan C++11, och så är det fortfarande, men nu om vi tittar på den här koden kommer vi att se att det inte finns mer kopiering här än i den första versionen, förutsatt att T har en flyttkonstruktör. De där. PPSC själv var och kommer att vara snabbare än PPZ, men koden använder på något sätt den godkända referensen, och ofta kopieras detta argument.

Detta är dock inte hela historien. Till skillnad från det första alternativet, där vi bara har kopiering, lägger vi även till rörelse här. Men att flytta är en billig operation, eller hur? Om det här ämnet har Mayers-boken som vi överväger också ett kapitel ("Artikel 29") som har titeln: "Anta att flyttoperationer inte finns, inte billiga och inte används." Huvudidén bör framgå av titeln, men om du vill ha detaljer, se till att läsa den - jag kommer inte att uppehålla mig vid den.

Det skulle här vara lämpligt att göra en fullständig jämförande analys av den första och sista metoden, men jag skulle inte vilja avvika från boken, så vi kommer att skjuta upp analysen för andra avsnitt, och här kommer vi att fortsätta att överväga Scotts argument. Så, bortsett från det faktum att det tredje alternativet uppenbarligen är kortare än det andra, vad ser Scott som fördelen med PPZ framför PPSC i modern kod?

Han ser det i det faktum att vid passerande av ett rvärde, dvs. vissa anropar så här: Holder holder(string("mig")); , alternativet med PPSC kommer att ge oss kopiering, och alternativet med PPZ kommer att ge oss rörelse. Å andra sidan, om överföringen är så här: Holder holder(someLvalue); , då förlorar PPZ definitivt på grund av att den kommer att utföra både kopiering och flyttning, medan det i versionen med PPSC bara kommer att finnas en kopiering. De där. det visar sig att PPZ, om vi betraktar rent effektivitet, är någon slags kompromiss mellan mängden kod och "fullständigt" (via && ) stöd för rörelsesemantik.

Det är därför Scott formulerade sina råd så noggrant och främjar det så noggrant. Det verkade till och med för mig att han tog upp det motvilligt, som om han var under press: han kunde inte låta bli att föra diskussioner om detta ämne i boken, eftersom... det diskuterades ganska brett, och Scott var alltid en samlare av kollektiv erfarenhet. Dessutom ger han väldigt få argument till försvar för PPZ, men han ger många av dem som ifrågasätter denna "teknik". Vi kommer att titta på hans argument mot i senare avsnitt, men här kommer vi kort att upprepa argumentet Scott gör till försvar av PPP (mentalt lägga till "om föremålet stödjer rörelse och det är billigt"): låter dig undvika kopiering när du skickar ett rvalue-uttryck som ett funktionsargument. Men nog med att plåga Meyers bok, låt oss gå vidare till en annan bok.

Förresten, om någon har läst boken och är förvånad över att jag inte inkluderar ett alternativ här med vad Mayers kallade universella referenser - nu känt som vidarebefordran av referenser - så är detta lätt att förklara. Jag överväger bara PPZ och PPSC, eftersom... Jag anser att det är dåligt att införa mallfunktioner för metoder som inte är mallar bara för att stödja hänvisning till båda typerna (rvalue/lvalue). För att inte tala om det faktum att koden blir annorlunda (ingen mer beständighet) och för med sig andra problem.

Josattis och sällskap

Den sista boken vi kommer att titta på är "C++ Templates", som också är den senaste av alla böcker som nämns i den här artikeln. Den publicerades i slutet av 2017 (och 2018 anges i boken). Till skillnad från andra böcker är den här helt tillägnad mönster, och inte råd (som Mayers) eller C++ i allmänhet, som Stroustrup. Därför övervägs fördelar/nackdelar här ur skrivmallars synvinkel.

Ett helt kapitel 7 ägnas åt detta ämne, som har den vältaliga titeln "By value or by reference?". I detta kapitel beskriver författarna ganska kort men koncist alla överföringsmetoder med alla deras för- och nackdelar. En analys av effektiviteten ges praktiskt taget inte här, och det tas för givet att PPSC kommer att vara snabbare än PPZ. Men med allt detta, i slutet av kapitlet rekommenderar författarna att du använder standard PPP för mallfunktioner. Varför? Eftersom att använda en länk visas mallparametrar helt och hållet, och utan en länk "förfaller de", vilket har en gynnsam effekt på bearbetningen av arrayer och strängliteraler. Författarna tror att om det för någon typ av PPP visar sig vara ineffektivt, så kan du alltid använda std::ref och std::cref . Detta är några råd, för att vara ärlig, har du sett många människor som vill använda ovanstående funktioner?

Vad ger de råd om PPSC? De rekommenderar att du använder PPSC när prestandan är kritisk eller det finns andra tung skäl att inte använda PPP. Naturligtvis pratar vi bara om boilerplate-kod här, men detta råd motsäger direkt allt som programmerare har lärt sig i ett decennium. Detta är inte bara ett råd att överväga PPP som ett alternativ - nej, det här är ett råd att göra PPSC till ett alternativ.

Detta avslutar vår bokturné, eftersom... Jag känner inte till några andra böcker som vi bör konsultera i denna fråga. Låt oss gå vidare till ett annat mediautrymme.

Nätverksvisdom

Därför att Vi lever i internets tidevarv, då bör du inte lita enbart på bokvisdom. Dessutom skriver många författare som brukade skriva böcker nu helt enkelt bloggar och har övergett böcker. En av dessa författare är Herb Sutter, som i maj 2013 publicerade en artikel på sin blogg "GotW #4 Solution: Class Mechanics", som, även om den inte helt ägnas åt problemet vi tar upp, fortfarande berör det.

Så i den ursprungliga versionen av artikeln upprepade Sutter helt enkelt den gamla visdomen: "passera parametrar med hänvisning till en konstant", men vi kommer inte längre att se den här versionen av artikeln, eftersom Artikeln innehåller det motsatta rådet: " Om parametern kommer fortfarande att kopieras och skicka den sedan efter värde." Återigen det ökända "om". Varför ändrade Sutter artikeln och hur fick jag reda på det? Från kommentarerna. Läs kommentarerna till hans artikel, de är förresten mer intressanta och användbara än själva artikeln. Det är sant att efter att ha skrivit artikeln ändrade Sutter äntligen sin åsikt och han ger inte längre sådana råd. Åsiktsförändringen återfinns i hans tal på CppCon 2014: ”Back to the Basics! Essentials of Modern C++ Style". Se till att titta, vi går vidare till nästa internetlänk.

Och härnäst har vi 2000-talets viktigaste programmeringsresurs: StackOverflow. Eller snarare svaret, med antalet positiva reaktioner som översteg 1700 när denna artikel skrevs. Frågan är: Vad är kopiera-och-byt-formspråket? , och, som titeln borde antyda, inte riktigt på ämnet vi tittar på. Men i sitt svar på denna fråga berör författaren också ett ämne som intresserar oss. Han rekommenderar också att man använder PPZ "om argumentet kommer att kopieras ändå" (det är dags att införa en förkortning för detta också, av Gud). Och generellt sett verkar detta råd ganska passande, inom ramen för hans svar och operatören som diskuteras där, men författaren tar sig friheten att ge sådana råd på ett bredare sätt, och inte bara i det här specifika fallet. Dessutom går han längre än alla tips vi tidigare har diskuterat och uppmanar till att göra detta även i C++03-kod! Vad fick författaren att dra sådana slutsatser?

Uppenbarligen hämtade författaren till svaret huvudinspirationen från en artikel av en annan bokförfattare och deltidsutvecklare av Boost.MPL - Dave Abrahams. Artikeln heter ”Vill du ha fart? Passera värde." , och den publicerades redan i augusti 2009, dvs. 2 år innan antagandet av C++11 och införandet av rörelsesemantik. Som i tidigare fall rekommenderar jag att läsaren läser artikeln på egen hand, men jag kommer att ge huvudargumenten (det finns faktiskt bara ett argument) som Dave ger till förmån för PPZ: du måste använda PPZ , eftersom "hoppa över kopia"-optimeringen fungerar bra med den (copy elision), som saknas i PPSC. Om du läser kommentarerna till artikeln kan du se att de råd han främjar inte är universella, vilket författaren själv bekräftar när han svarar på kritik från kommentatorer. Däremot innehåller artikeln uttryckliga råd (riktlinje) att använda PPP om argumentet ändå kommer att kopieras. Förresten, om någon är intresserad kan du läsa artikeln ”Vill du ha fart? Förbigå inte (alltid) värde." . Som rubriken borde antyda är den här artikeln ett svar på Daves artikel, så om du läser den första, se till att läsa den här också!

Tyvärr (lyckligtvis för vissa) ger sådana artiklar och (ännu mer så) populära svar på populära webbplatser upphov till den massiva användningen av tvivelaktiga tekniker (ett trivialt exempel) helt enkelt för att detta kräver mindre skrivning, och den gamla dogmen är inte längre orubblig - Du kan alltid hänvisa till "det där populära rådet" om du trycks mot väggen. Nu föreslår jag att du bekantar dig med vad olika resurser erbjuder oss med rekommendationer för att skriva kod.

Därför att Eftersom olika standarder och rekommendationer nu också publiceras online, bestämde jag mig för att klassificera detta avsnitt som "nätverksvisdom." Så här skulle jag vilja prata om två källor, vars syfte är att göra C++-programmerares kod bättre genom att förse dem med tips (riktlinjer) om hur man skriver just denna kod.

Den första uppsättningen regler som jag vill överväga var droppen som tvingade mig att ta upp den här artikeln. Den här uppsättningen är en del av det klangstäda verktyget och finns inte utanför det. Precis som allt relaterat till clang är det här verktyget väldigt populärt och har redan fått integration med CLion och Resharper C++ (det var så jag kom över det). Så, clang-tydy innehåller en regel för modernisera-pass-för-värde som fungerar på konstruktörer som accepterar argument via PPSC. Denna regel föreslår att vi ersätter PPSC med PPZ. Dessutom innehåller beskrivningen av denna regel vid tidpunkten för att skriva artikeln en anmärkning om att denna regel Hejdå fungerar bara för konstruktörer, men de (vem är de?) tar gärna emot hjälp från de som utökar denna regel till andra enheter. Där, i beskrivningen, finns också en länk till Daves artikel - det är tydligt var benen kommer ifrån.

Till sist, för att avsluta denna genomgång av andra människors visdom och auktoritativa åsikter, föreslår jag att du tittar på de officiella riktlinjerna för att skriva C++-kod: C++ Core Guidelines, vars huvudredaktörer är Herb Sutter och Bjarne Stroustrup (inte dåligt, eller hur?). Så dessa rekommendationer innehåller följande regel: "För "in" parametrar, skicka billigt kopierade typer efter värde och andra med hänvisning till const", vilket helt upprepar den gamla visdomen: PPSK överallt och PPP för små objekt. Det här tipset beskriver flera alternativ att överväga. om argumentet passerar behöver optimeras. Men PPZ finns inte med i listan över alternativ!

Eftersom jag inte har några andra källor värda att uppmärksamma, föreslår jag att gå vidare till en direkt analys av båda överföringsmetoderna.

Analys

Hela föregående text är skriven på ett sätt som är något ovanligt för mig: jag framför andras åsikter och försöker till och med att inte uttrycka mina egna (jag vet att det blir dåligt). Till stor del på grund av att andras åsikter, och mitt mål var att göra en kort översikt över dem, sköt jag upp en detaljerad övervägande av vissa argument som jag hittade hos andra författare. I det här avsnittet kommer jag inte att hänvisa till myndigheter och ge åsikter, här ska vi titta på några objektiva fördelar och nackdelar med PPSC och PPZ, som kommer att kryddas med min subjektiva uppfattning. Naturligtvis kommer en del av det som diskuterades tidigare att upprepas, men tyvärr, detta är strukturen i den här artikeln.

Har PPP en fördel?

Så, innan jag överväger argumenten för och emot, föreslår jag att vi tittar på vad och i vilka fall fördelen med att passera värdet ger oss. Låt oss säga att vi har en klass så här:

Klass CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotMover(And_NotMalVal) (And_NotMalVal av erAndNotMover; ) void setRvaluer (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Även om vi för den här artikeln bara är intresserade av de två första funktionerna, har jag inkluderat fyra alternativ bara för att använda dem som en kontrast.

Accounter-klassen är en enkel klass som räknar hur många gånger den har kopierats/flyttats. Och i CopyMover-klassen har vi implementerat funktioner som låter oss överväga följande alternativ:

    rör på sig klarat argument.

    Passera efter värde, följt av kopiering klarat argument.

Om vi ​​nu skickar ett lvärde till var och en av dessa funktioner, till exempel så här:

Accounter byRefer; Accounter byValuer; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

då får vi följande resultat:

Den självklara vinnaren är PPSC, eftersom... ger bara ett exemplar, medan PPZ ger ett exemplar och ett drag.

Låt oss nu försöka skicka ett rvärde:

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

Vi får följande:

Det finns ingen klar vinnare här, för... både PPZ och PPSK har en operation vardera, men på grund av att PPZ använder rörelse, och PPSK använder kopiering, kan vi ge segern till PPZ.

Men våra experiment slutar inte där; låt oss lägga till följande funktioner för att simulera ett indirekt anrop (med efterföljande argument som skickas):

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

Vi kommer att använda dem på exakt samma sätt som vi gjorde utan dem, så jag kommer inte att upprepa koden (titta i förvaret om det behövs). Så för lvalue skulle resultaten bli så här:

Observera att PPSC ökar gapet med PPZ, kvar med en enda kopia, medan PPZ redan har så många som 3 operationer (en rörelse till)!

Nu passerar vi rvärdet och får följande resultat:

Nu har PPZ 2 rörelser, och PPSC har fortfarande en kopia. Är det nu möjligt att utse PPZ till vinnare? Nej, för att om ett drag åtminstone inte skulle vara sämre än en kopia, kan vi inte säga detsamma om 2 drag. Därför kommer det inte att finnas någon vinnare i detta exempel.

De kan invända mot mig: "Författare, du har en partisk åsikt och du drar in det som är fördelaktigt för dig. Även två drag kommer att vara billigare än att kopiera!” Jag kan inte hålla med om detta uttalande Allt som allt, därför att Hur mycket snabbare flytt är än att kopiera beror på den specifika klassen, men vi ska titta på "billig" flyttning i ett separat avsnitt.

Här berörde vi en intressant sak: vi lade till ett indirekt samtal och PPP lade till exakt en operation i "vikt". Jag tror att du inte behöver ha ett diplom från MSTU för att förstå att ju fler indirekta samtal vi har, desto fler operationer kommer att utföras när du använder PPZ, medan för PPSC kommer numret att förbli oförändrat.

Allt som diskuterats ovan var osannolikt att vara en uppenbarelse för någon, vi kanske inte ens hade utfört experiment - alla dessa siffror borde vara uppenbara för de flesta C++-programmerare vid första anblicken. Det är sant att en punkt fortfarande förtjänar ett förtydligande: varför, när det gäller rvalue, har PZ inte en kopia (eller ett annat drag), utan bara ett drag.

Tja, vi tog en titt på skillnaden i överföring mellan PPZ och PPSC genom att i första hand observera antalet kopior och drag. Även om det är uppenbart att fördelen med PPZ framför PPSC även i så enkla exempel är milt uttryckt Inte Uppenbarligen drar jag fortfarande, lite pretentiöst, följande slutsats: om vi fortfarande ska kopiera funktionsargumentet, är det vettigt att överväga att överföra argumentet till funktionen efter värde. Varför drog jag denna slutsats? För att smidigt gå vidare till nästa avsnitt.

Om vi ​​kopierar...

Så vi kommer till det ökända "om". De flesta av argumenten vi stötte på krävde inte universell implementering av PPP istället för PPSC; de krävde bara att göra det "om argumentet ändå kopieras." Det är dags att ta reda på vad som är fel med detta argument.

Jag vill börja med en liten beskrivning av hur jag skriver kod. På sistone har min kodningsprocess blivit mer och mer lik TDD, d.v.s. att skriva valfri klassmetod börjar med att skriva ett test där denna metod förekommer. Följaktligen, när jag börjar skriva ett test och skapar en metod efter att ha skrivit testet, vet jag fortfarande inte om jag kommer att kopiera argumentet. Naturligtvis skapas inte alla funktioner på detta sätt, ofta, även när du skriver ett test, vet du exakt vilken typ av implementering det kommer att finnas. Men detta händer inte alltid!

Någon kanske invänder mot mig att det inte spelar någon roll hur metoden ursprungligen skrevs, vi kan ändra hur vi förmedlar argumentet när metoden har tagit form och det är helt klart för oss vad som händer där (dvs om vi har kopiering eller inte). Jag håller delvis med om detta - visserligen kan du göra det på det här sättet, men det här involverar oss i något slags konstigt spel där vi måste byta gränssnitt bara för att implementeringen har förändrats. Vilket för oss till nästa dilemma.

Det visar sig att vi modifierar (eller till och med planerar) gränssnittet baserat på hur det kommer att implementeras. Jag anser mig inte vara en expert på OOP och andra teoretiska beräkningar av mjukvaruarkitektur, men sådana åtgärder strider tydligt mot de grundläggande reglerna när implementeringen inte ska påverka gränssnittet. Naturligtvis läcker vissa implementeringsdetaljer (oavsett om de är funktioner i språket eller målplattformen) fortfarande genom gränssnittet på ett eller annat sätt, men du bör försöka minska, inte öka, antalet sådana saker.

Nåväl, Gud välsigne honom, låt oss gå den här vägen och ändå ändra gränssnitten beroende på vad vi gör i implementeringen när det gäller att kopiera argumentet. Låt oss säga att vi skrev den här metoden:

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

och genomförde våra ändringar i arkivet. Allteftersom tiden gick fick vår mjukvaruprodukt ny funktionalitet, nya ramverk integrerades och uppgiften uppstod att informera omvärlden om förändringar i vår klass. De där. Vi kommer att lägga till någon aviseringsmekanism till vår metod, låt det vara något som liknar Qt-signaler:

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

Finns det något problem med den här koden? Äta. För varje anrop till setName skickar vi en signal, så signalen kommer att skickas även när menande m_Name har inte ändrats. Förutom prestandaproblem kan denna situation leda till en oändlig loop på grund av att koden som tar emot ovanstående meddelande på något sätt kommer att anropa setName . För att undvika alla dessa problem ser sådana metoder oftast ut ungefär så här:

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

Vi blev av med problemen som beskrivits ovan, men nu har vår "om vi kopierar ändå..."-regeln misslyckats - det finns inte längre ovillkorlig kopiering av argumentet, nu kopierar vi det bara om det ändras! Så vad ska vi göra nu? Ändra gränssnittet? Okej, låt oss ändra klassgränssnittet på grund av denna fix. Tänk om vår klass ärvde den här metoden från något abstrakt gränssnitt? Låt oss ändra det där också! Är det många förändringar eftersom implementeringen har förändrats?

Återigen kan de invända mot mig, säger de, författaren, varför försöker du spara pengar på matcher när det här tillståndet kommer att fungera där ute? Ja, de flesta samtalen kommer att vara falska! Finns det något förtroende för detta? Var? Och om jag bestämde mig för att spara på matcher, var inte själva det faktum att vi använde PPZ en konsekvens av just sådana besparingar? Jag fortsätter bara "partilinjen" som förespråkar effektivitet.

Konstruktörer

Låt oss kort gå igenom konstruktörer, särskilt eftersom det finns en speciell regel för dem i clang-tidy, som ännu inte fungerar för andra metoder/funktioner. Låt oss säga att vi har en klass så här:

Klass JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) privat: sträng m_JustString; );

Uppenbarligen kopieras parametern, och clang-tidy kommer att berätta för oss att det skulle vara en bra idé att skriva om konstruktorn till detta:

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

Och ärligt talat, det är svårt för mig att argumentera här - trots allt kopierar vi verkligen alltid. Och oftast, när vi skickar något genom en konstruktör, kopierar vi det. Men oftare betyder inte alltid. Här är ett annat exempel:

Class 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; );

Här kopierar vi inte alltid, utan bara när datumen presenteras korrekt. Naturligtvis kommer detta att vara fallet i de allra flesta fall. Men inte alltid.

Du kan ge ett annat exempel, men denna gång utan kod. Föreställ dig att du har en klass som accepterar ett stort föremål. Klassen har funnits länge, och nu är det dags att uppdatera implementeringen. Vi inser att vi inte behöver mer än hälften av en stor anläggning (som har växt under åren), och kanske ännu mindre. Kan vi göra något åt ​​detta genom att ha ett värde? Nej, vi kommer inte att kunna göra någonting, eftersom en kopia fortfarande kommer att skapas. Men om vi använde PPSC skulle vi helt enkelt ändra vad vi gör inuti designer. Och detta är nyckelpunkten: med PPSC kontrollerar vi vad och när som händer i implementeringen av vår funktion (konstruktor), men om vi använder PPZ förlorar vi all kontroll över kopiering.

Vad kan du ta med dig från det här avsnittet? Det faktum att argumentet "om vi kopierar ändå..." är mycket kontroversiellt, eftersom Vi vet inte alltid vad vi kommer att kopiera, och även när vi vet är vi ofta inte säkra på att detta kommer att fortsätta i framtiden.

Att flytta är billigt

Från det ögonblick som rörelsens semantik dök upp började den ha ett allvarligt inflytande på hur modern C++-kod skrivs, och med tiden har detta inflytande bara intensifierats: det är inte konstigt, eftersom rörelse är så billig jämfört med kopiering. Men är det? Är det sant att rörelse är Alltid billig operation? Detta är vad vi kommer att försöka ta reda på i det här avsnittet.

Binärt stort objekt

Låt oss börja med ett trivialt exempel, låt oss säga att vi har följande klass:

Struct Blob ( std::array data; );

Vanlig klick(BDO, engelska BLOB), som kan användas i en mängd olika situationer. Låt oss titta på vad det kommer att kosta oss att passera genom referens och värde. Vår BDO kommer att användas ungefär så här:

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

Och vi kommer att kalla dessa funktioner så här:

Const Blob blob(); lagring; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Koden för andra exempel kommer att vara identisk med den här, bara med olika namn och typer, så jag kommer inte att ge den för de återstående exemplen - allt finns i förvaret.

Innan vi går vidare till mätningar, låt oss försöka förutsäga resultatet. Så vi har en 4 KB std::array som vi vill lagra i ett Storage-klassobjekt. Som vi fick reda på tidigare, för PPSC kommer vi att ha en kopia, medan vi för PPZ kommer att ha en kopia och en flytt. Baserat på det faktum att det är omöjligt att flytta arrayen kommer det att finnas 2 kopior för PPZ, mot en för PPSC. De där. vi kan förvänta oss en dubbel överlägsenhet i prestanda för PPSC.

Låt oss nu ta en titt på testresultaten:

Detta och alla efterföljande tester kördes på samma maskin med MSVS 2017 (15.7.2) och flaggan /O2.

Övning sammanföll med antagandet - att passera efter värde är 2 gånger dyrare, eftersom för en array är flyttning helt likvärdig med kopiering.

Linje

Låt oss titta på ett annat exempel, en vanlig std::string . Vad kan vi förvänta oss? Vi vet (jag diskuterade detta i artikeln) att moderna implementeringar skiljer mellan två typer av strängar: kort (cirka 16 tecken) och lång (de som är längre än korta). För korta används en intern buffert, som är en vanlig C-array av char, men långa kommer redan att placeras på högen. Vi är inte intresserade av korta rader, eftersom... resultatet där blir detsamma som med BDO, så låt oss fokusera på långa rader.

Så med en lång sträng är det uppenbart att det borde vara ganska billigt att flytta det (flytta bara på pekaren), så du kan lita på det faktum att att flytta strängen inte bör påverka resultaten alls, och PPZ bör ge ett resultat inte värre än PPSC. Låt oss kontrollera det i praktiken och få följande resultat:

Vi kommer att gå vidare till att förklara detta "fenomen". Så vad händer när vi kopierar en befintlig sträng till en redan befintlig sträng? Låt oss titta på ett trivialt exempel:

Sträng först(64, "C"); sträng sekund(64, "N"); //... andra = första;

Vi har två strängar på 64 tecken, så den interna bufferten är otillräcklig när du skapar dem, vilket resulterar i att båda strängarna allokeras på högen. Nu kopierar vi först till andra. Därför att våra radstorlekar är desamma, uppenbarligen finns det tillräckligt med utrymme tilldelat i andra för att rymma all data från första, så andra = första; kommer att bli en banal memcpy, inget mer. Men om vi tittar på ett något modifierat exempel:

Sträng först(64, "C"); sträng andra = första;

då kommer det inte längre att finnas ett anrop till operator=, utan kopieringskonstruktorn kommer att anropas. Därför att Eftersom vi har att göra med en konstruktor finns det inget befintligt minne i den. Den måste först väljas och först sedan kopieras först. De där. detta är minnesallokering och sedan memcpy. Som du och jag vet är att allokera minne på den globala högen vanligtvis en dyr operation, så att kopiera från det andra exemplet blir dyrare än att kopiera från det första. Dyrare minnesallokering per hög.

Vad har detta med vårt ämne att göra? Det mest direkta, eftersom det första exemplet visar exakt vad som händer med PPSC, och det andra visar vad som händer med PPZ: för PPZ skapas alltid en ny rad, medan för PPSC återanvänds den befintliga. Du har redan sett skillnaden i exekveringstid, så det finns inget att tillägga här.

Även här ställs vi inför det faktum att när vi använder PPP arbetar vi ur vårt sammanhang och kan därför inte använda alla fördelar som det kan ge. Och om vi tidigare resonerat i termer av teoretiska framtida förändringar, så ser vi här ett mycket konkret misslyckande i produktiviteten.

Naturligtvis kan någon invända mot mig att strängen skiljer sig, och de flesta typer fungerar inte på det sättet. Till vilket jag kan svara följande: allt som beskrivits tidigare kommer att vara sant för alla behållare som allokerar minne i högen omedelbart för ett paket med element. Dessutom, vem vet vilka andra kontextkänsliga optimeringar som används i andra typer?

Vad ska du ta med dig från det här avsnittet? Att även om flyttningen är riktigt billig betyder inte att att ersätta kopiering med copy+moving alltid ger ett resultatmässigt jämförbart resultat.

Komplex typ

Låt oss slutligen titta på en typ som kommer att bestå av flera objekt. Låt detta vara klassen Person, som består av data som är inneboende i en person. Vanligtvis är detta ditt förnamn, efternamn, postnummer osv. Du kan representera allt detta som strängar och anta att strängarna du lägger i fälten i klassen Person sannolikt är korta. Även om jag tror att det i verkligheten kommer att vara mest användbart att mäta korta strängar, kommer vi fortfarande att titta på strängar i olika storlekar för att ge en mer komplett bild.

Jag kommer också att använda Person med 10 fält, men för detta kommer jag inte skapa 10 fält direkt i klasskroppen. Implementeringen av Person döljer en container i dess djup - detta gör det bekvämare att ändra testparametrar, praktiskt taget utan att avvika från hur det skulle fungera om Person var en riktig klass. Implementeringen är dock tillgänglig och du kan alltid kontrollera koden och berätta om jag gjort något fel.

Så, låt oss gå: Person med 10 fält av typen string , som vi överför med PPSC och PPZ till Storage:

Som du kan se har vi en enorm skillnad i prestanda, vilket inte borde komma som en överraskning för läsarna efter de tidigare avsnitten. Jag tror också att klassen Person är tillräckligt "riktig" för att sådana resultat inte kommer att avfärdas som abstrakta.

Förresten, när jag förberedde den här artikeln förberedde jag ett annat exempel: en klass som använder flera std::function-objekt. Enligt min idé skulle det också visa en fördel i PPSC:s prestanda framför PPZ, men det blev precis tvärtom! Men jag ger inte det här exemplet här, inte för att jag inte gillade resultaten, utan för att jag inte hade tid att ta reda på varför sådana resultat erhölls. Ändå finns det kod i förvaret (skrivare), tester - också, om någon vill ta reda på det skulle jag gärna höra om resultaten av forskningen. Jag planerar att återkomma till det här exemplet senare, och om ingen publicerar dessa resultat före mig, kommer jag att överväga dem i en separat artikel.

Resultat

Så vi har tittat på de olika för- och nackdelarna med att passera efter värde och passera med hänvisning till en konstant. Vi tittade på några exempel och tittade på prestandan för båda metoderna i dessa exempel. Naturligtvis kan och är den här artikeln inte uttömmande, men enligt min mening innehåller den tillräckligt med information för att kunna fatta ett oberoende och välgrundat beslut om vilken metod som är bäst att använda. Någon kan invända: "varför använda en metod, låt oss börja från uppgiften!" Även om jag håller med om denna tes i allmänhet, håller jag inte med om den i den här situationen. Jag tror att det bara kan finnas ett sätt att föra argument på ett språk, som används som standard.

Vad betyder standard? Det betyder att när jag skriver en funktion så tänker jag inte på hur jag ska klara argumentet, jag använder bara "default". C++-språket är ett ganska komplext språk som många undviker. Och enligt min åsikt beror komplexiteten inte så mycket på komplexiteten i språkkonstruktionerna som finns i språket (en typisk programmerare kanske aldrig stöter på dem), utan av det faktum att språket får dig att tänka mycket: har jag frigjort upp minne, är det dyrt att använda den här funktionen och så vidare.

Många programmerare (C, C++ och andra) är misstroende och rädda för C++ som började dyka upp efter 2011. Jag har hört mycket kritik om att språket blir mer komplext, att bara "guruer" nu kan skriva i det osv. Personligen anser jag att det inte är så – tvärtom ägnar kommittén mycket tid åt att göra språket mer vänligt för nybörjare och så att programmerare behöver tänka mindre på språkets funktioner. När allt kommer omkring, om vi inte behöver kämpa med språket, då har vi tid att tänka på uppgiften. Dessa förenklingar inkluderar smarta pekare, lambda-funktioner och mycket mer som dök upp i språket. Samtidigt förnekar jag inte att vi nu behöver plugga mer, men vad är det för fel på att plugga? Eller händer det inga förändringar på andra populära språk som behöver läras?

Vidare tvivlar jag inte på att det kommer att finnas snobbar som kan svara: "Du vill inte tänka? Gå sedan och skriv i PHP." Jag vill inte ens svara till sådana människor. Jag ska bara ge ett exempel från spelverkligheten: i den första delen av Starcraft, när en ny arbetare skapas i en byggnad, för att han skulle börja utvinna mineraler (eller gas), var han tvungen att skickas dit manuellt. Dessutom hade varje förpackning av mineraler en gräns, då ökningen av arbetare var värdelös, och de kunde till och med störa varandra och försämra produktionen. Detta ändrades i Starcraft 2: arbetare börjar automatiskt bryta mineraler (eller gas), och det indikerar också hur många arbetare som för närvarande bryter och hur mycket som är gränsen för denna fyndighet. Detta förenklade avsevärt spelarens interaktion med basen, vilket gjorde att han kunde fokusera på viktigare aspekter av spelet: bygga en bas, samla trupper och förstöra fienden. Det verkar som att detta bara är en stor innovation, men vad började på Internet! Människor (vem är de?) började skrika att spelet "höll på att skruvas ihop" och "de dödade Starcraft." Uppenbarligen kunde sådana meddelanden bara komma från "bevarare av hemlig kunskap" och "adepter av hög APM" som gillade att vara i någon "elitklubb".

Så, för att återgå till vårt ämne, ju mindre jag behöver tänka på hur man skriver kod, desto mer tid har jag att tänka på att lösa det omedelbara problemet. Att fundera på vilken metod jag ska använda - PPSC eller PPZ - för mig inte ett dugg närmare att lösa problemet, så jag vägrar helt enkelt att tänka på sådana saker och väljer ett alternativ: att hänvisa till en konstant. Varför? Eftersom jag inte ser några fördelar med PPP i allmänna fall, och speciella fall måste övervägas separat.

Det är ett specialfall, det är bara det, efter att ha märkt att PPSC i någon metod visar sig vara en flaskhals, och genom att ändra överföringen till PPZ, kommer vi att få en viktig ökning av prestanda, jag tvekar inte att använda PPZ. Men som standard kommer jag att använda PPSC både i vanliga funktioner och i konstruktörer. Och om möjligt kommer jag att marknadsföra just denna metod där det är möjligt. Varför? För jag tycker att praxis att främja PPP är ond på grund av det faktum att lejonparten av programmerare inte är särskilt kunniga (antingen i princip, eller helt enkelt ännu inte kommit igång med saker och ting), och de följer helt enkelt råd. Plus, om det finns flera motstridiga råd väljer de det som är enklare, och detta leder till pessimism i koden helt enkelt för att någon någonstans hört något. Åh ja, den här någon kan också ge en länk till Abrahams artikel för att bevisa att han har rätt. Och sedan sitter du, läser koden och tänker: är det faktum att parametern skickas av värde här för att programmeraren som skrev detta kom från Java, bara läst en massa "smarta" artiklar, eller finns det verkligen ett behov av en teknisk specifikation?

PPSC är mycket lättare att läsa: personen känner tydligt till den "goda formen" av C++ och vi går vidare - blicken dröjer inte kvar. Användningen av PPSC har lärts ut för C++-programmerare i flera år, vad är anledningen till att överge det? Detta leder mig till en annan slutsats: om ett metodgränssnitt använder en PPP, så bör det också finnas en kommentar varför det är så. I andra fall måste PPSC tillämpas. Naturligtvis finns det undantagstyper, men jag nämner dem inte här bara för att de är underförstådda: string_view , initializer_list , olika iteratorer, etc. Men dessa är undantag, vars lista kan utökas beroende på vilka typer som används i projektet. Men essensen förblir densamma sedan C++98: som standard använder vi alltid PPCS.

För std::string blir det med största sannolikhet ingen skillnad på små strängar, vi kommer att prata om detta senare.

Jag ber på förhand om ursäkt för den pretentiösa kommentaren om att "placera poäng", men vi måste på något sätt locka in dig i artikeln)) För min del kommer jag att försöka se till att sammanfattningen fortfarande uppfyller dina förväntningar.

Kortfattat vad vi pratar om

Alla vet redan detta, men i början ska jag påminna dig om hur metodparametrar kan skickas i 1C. De kan skickas "genom referens" eller "efter värde". I det första fallet skickar vi till metoden samma värde som vid anropspunkten, och i det andra en kopia av det.

Som standard, i 1C, skickas argument genom referens, och ändringar av en parameter inuti en metod kommer att vara synliga utanför metoden. Här beror ytterligare förståelse av frågan på vad exakt du förstår med ordet "ändring av parameter". Så detta innebär omplacering och inget mer. Dessutom kan tilldelningen vara implicit, till exempel anropa en plattformsmetod som returnerar något i outputparametern.

Men om vi inte vill att vår parameter ska skickas genom referens, kan vi ange ett nyckelord före parametern Menande

Procedur ByValue(Value Parameter) Parameter = 2; EndProcedure Parameter = 1; ByValue(Parameter); Rapport(parameter); // kommer att skriva ut 1

Allt fungerar som utlovat - att ändra (eller snarare "byta ut") parametervärdet ändrar inte värdet utanför metoden.

Vad är skämtet?

Intressanta ögonblick börjar när vi börjar skicka inte primitiva typer (strängar, tal, datum, etc.) som parametrar, utan objekt. Det är här begrepp som "grunda" och "djupa" kopior av ett objekt kommer in i bilden, liksom pekare (inte i C++-termer, utan som abstrakta handtag).

När vi skickar ett objekt (till exempel en värdetabell) genom referens skickar vi själva pekarvärdet (ett visst handtag), som "håller" objektet i plattformens minne. När värdet passerats kommer plattformen att göra en kopia av denna pekare.

Med andra ord, om vi, genom att skicka ett objekt genom referens, i en metod tilldelar värdet "Array" till parametern, kommer vi vid anropspunkten att få en array. Omtilldelningen av värdet som skickats genom referens är synlig från samtalsplatsen.

Procedur ProcessValue(Parameter) Parameter = New Array; EndProcedure Table = New ValueTable; ProcessValue(Tabell); Report(ValueType(Table)); // kommer att mata ut en Array

Om vi ​​skickar objektet efter värde, kommer vår värdetabell inte att gå förlorad vid anropet.

Objektets innehåll och tillstånd

När man passerar efter värde kopieras inte hela objektet, utan bara dess pekare. Objektinstansen förblir densamma. Det spelar ingen roll hur du skickar objektet, genom referens eller värde - genom att rensa värdetabellen rensas själva tabellen. Denna rengöring kommer att synas överallt, eftersom... det fanns bara ett objekt och det spelade ingen roll hur exakt det överfördes till metoden.

Procedur ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Table.Add(); ProcessValue(Tabell); Rapport(Tabell.Quantity()); // kommer att mata ut 0

När objekt skickas till metoder, arbetar plattformen med pekare (villkorliga, inte direkta analoger från C++). Om ett objekt skickas med referens, kan minnescellen i den virtuella 1C-maskinen som objektet ligger i skrivas över av ett annat objekt. Om ett objekt passeras av ett värde, kopieras pekaren och överskrivning av objektet resulterar inte i att minnesplatsen skrivs över med det ursprungliga objektet.

Samtidigt alla förändringar stat objekt (rengöring, tillägg av egenskaper etc.) ändrar själva objektet och har ingenting alls att göra med hur och var objektet överfördes. Tillståndet för en objektinstans har ändrats; det kan finnas ett gäng "referenser" och "värden" till den, men instansen är alltid densamma. Genom att skicka ett objekt till en metod skapar vi inte en kopia av hela objektet.

Och detta är alltid sant, förutom...

Klient-server-interaktion

Plattformen implementerar serveranrop mycket transparent. Vi anropar helt enkelt en metod, och under huven serialiserar plattformen (förvandlas till en sträng) alla parametrar för metoden, skickar dem till servern och returnerar sedan utdataparametrarna tillbaka till klienten, där de deserialiseras och lever som om de aldrig hade varit på någon server.

Som du vet är inte alla plattformsobjekt serialiserbara. Det är här begränsningen växer: inte alla objekt kan skickas till servermetoden från klienten. Om du passerar ett objekt som inte går att serialisera kommer plattformen att börja använda dåliga ord.

  • En uttrycklig förklaring av programmerarens avsikter. Genom att titta på metodsignaturen kan du tydligt se vilka parametrar som matas in och vilka som matas ut. Denna kod är lättare att läsa och underhålla
  • För att en ändring av parametern "by reference" på servern ska vara synlig vid anropspunkten på klienten, p. Plattformen själv kommer nödvändigtvis att returnera parametrarna som skickas till servern via länk till klienten för att säkerställa beteendet som beskrivs i början av artikeln. Om parametern inte behöver returneras kommer det att bli trafiköverskridande. För att optimera datautbytet bör parametrar vars värden vi inte behöver vid utgången markeras med ordet Value.

Den andra punkten är anmärkningsvärd här. För att optimera trafiken kommer plattformen inte att returnera parametervärdet till klienten om parametern är markerad med ordet Value. Det här är bra, men det leder till en intressant effekt.

Som jag redan sa, när ett objekt överförs till servern sker serialisering, d.v.s. en "djup" kopia av objektet utförs. Och om det finns ett ord Menande objektet kommer inte att resa från servern tillbaka till klienten. Vi lägger till dessa två fakta och får följande:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= Nya ListValues; List1.Add("hej"); List2 = List1.Copy(); List3 = List1.Copy(); // objektet kopieras helt, // överförs till servern och returneras sedan. // att rensa listan är synlig vid anropspunkten ByRef(List1); // objektet kopieras helt, // överförs till servern. Det kommer inte tillbaka. // Att rensa listan är INTE VISIBLE vid anropet av ByValue(List2); // endast objektpekaren kopieras // att rensa listan är synlig vid punkten för anropet till ByValueClient(List3); Report(List1.Quantity()); Report(List2.Quantity()); Report(List3.Quantity()); Slut på förfarandet

Sammanfattning

Kortfattat kan det sammanfattas så här:

  • Genom att passera genom referens kan du "skriva över" ett objekt med ett helt annat objekt
  • Att passera genom värde tillåter dig inte att "skriva över" objektet, men ändringar i objektets interna tillstånd kommer att vara synliga, eftersom vi arbetar med samma objektinstans
  • När man gör ett serveranrop jobbar man med OLIKA instanser av objektet, eftersom En djup kopia utfördes. Nyckelord Menande kommer att förhindra att serverinstansen kopieras tillbaka till klientinstansen, och att ändra det interna tillståndet för ett objekt på servern kommer inte att leda till en liknande ändring på klienten.

Jag hoppas att denna enkla lista med regler kommer att göra det enklare för dig att lösa tvister med kollegor angående överföring av parametrar "efter värde" och "genom referens"