Prejdite podľa hodnoty. Odovzdávanie parametrov odkazom a hodnotou. Predvolené nastavenia

Nech je teda faktoriál(n) funkciou na výpočet faktoriálu čísla n. Potom, keďže „vieme“ faktoriál 1 je 1, môžeme zostaviť nasledujúci reťazec:

Faktor(4) = Faktor(3)*4

Faktor(3) = Faktor(2)*3

Faktor(2) = Faktor(1)*2

Ak by sme však nemali terminálnu podmienku, že keď n=1, faktoriálna funkcia by mala vrátiť 1, potom by sa takýto teoretický reťazec nikdy neskončil a mohlo ísť o chybu pretečenia zásobníka volaní – pretečenie zásobníka hovorov. Aby sme pochopili, čo je zásobník hovorov a ako môže pretekať, pozrime sa na rekurzívnu implementáciu našej funkcie:

Faktor funkcie (n: Integer): LongInt;

Ak n=1 Potom

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

Koniec;

Ako vidíme, aby reťazec fungoval správne, pred každým ďalším volaním funkcie je potrebné niekam uložiť všetky lokálne premenné, aby pri obrátení reťazca bol výsledok správny (vypočítaná hodnota faktoriálu n-1 sa vynásobí n ). V našom prípade zakaždým, keď sa faktoriálna funkcia volá zo seba, musia sa uložiť všetky hodnoty premennej n. Oblasť, v ktorej sú uložené lokálne premenné funkcie pri jej rekurzívnom volaní, sa nazýva zásobník hovorov. Samozrejme, tento zásobník nie je nekonečný a môže sa vyčerpať, ak sú rekurzívne volania skonštruované nesprávne. Konečnosť iterácií nášho príkladu je zaručená tým, že keď n=1, volanie funkcie sa zastaví.

Odovzdávanie parametrov hodnotou a odkazom

Doteraz sme nemohli zmeniť hodnotu v podprograme skutočný parameter(t. j. parameter, ktorý je zadaný pri volaní podprogramu) a v niektorých aplikačných úlohách by to bolo vhodné. Spomeňme si na procedúru Val, ktorá mení hodnotu dvoch svojich skutočných parametrov naraz: prvým je parameter, kam sa bude zapisovať konvertovaná hodnota reťazcovej premennej a druhým je parameter Code, kde sa číslo chybného znak sa umiestni v prípade zlyhania pri konverzii typu. Tie. stále existuje mechanizmus, ktorým môže podprogram meniť skutočné parametre. To je možné vďaka rôznym spôsobom odovzdávania parametrov. Pozrime sa bližšie na tieto metódy.

Programovanie v Pascale

Odovzdávanie parametrov hodnotou

V podstate sme takto odovzdali všetky parametre našim rutinám. Mechanizmus je nasledovný: keď je zadaný aktuálny parameter, jeho hodnota sa skopíruje do oblasti pamäte, kde sa nachádza podprogram a potom, keď funkcia alebo procedúra dokončí svoju prácu, táto oblasť sa vymaže. Zhruba povedané, keď je podprogram spustený, existujú dve kópie jeho parametrov: jedna v rozsahu volajúceho programu a druhá v rozsahu funkcie.

Pri tomto spôsobe odovzdávania parametrov trvá volanie podprogramu viac času, pretože okrem samotného volania je potrebné skopírovať všetky hodnoty všetkých skutočných parametrov. Ak sa do podprogramu odovzdá veľké množstvo údajov (napríklad pole s veľkým počtom prvkov), čas potrebný na skopírovanie údajov do lokálnej oblasti môže byť značný a treba to vziať do úvahy pri vývoji programov a nájsť prekážky v ich výkone.

Pri tomto spôsobe prenosu nemôže podprogram meniť skutočné parametre, pretože zmeny ovplyvnia iba izolovanú lokálnu oblasť, ktorá sa uvoľní po dokončení funkcie alebo procedúry.

Odovzdávanie parametrov odkazom

Pri tejto metóde sa hodnoty skutočných parametrov neskopírujú do podprogramu, ale prenesú sa adresy v pamäti (odkazy na premenné), kde sa nachádzajú. V tomto prípade podprogram už mení hodnoty, ktoré nie sú v lokálnom rozsahu, takže všetky zmeny budú viditeľné pre volajúci program.

Na označenie, že argument musí byť odovzdaný odkazom, sa pred jeho deklaráciu pridá kľúčové slovo var:

Procedure getTwoRandom(var n1, n2:Integer; rozsah: Integer);

n1:=random(rozsah);

n2:=random(rozsah); koniec ;

var rand1, rand2: celé číslo;

Začať getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Koniec.

V tomto príklade sú odkazy na dve premenné odovzdané procedúre getTwoRandom ako skutočné parametre: rand1 a rand2. Tretí aktuálny parameter (10) prechádza hodnotou. Procedúra zapisuje pomocou formálnych parametrov

Metódy programovania pomocou reťazcov

Účel laboratórnej práce : naučiť sa metódy v jazyku C#, pravidlá pre prácu so znakovými údajmi a komponent ListBox. Napíšte program na prácu s reťazcami.

Metódy

Metóda je prvok triedy, ktorý obsahuje programový kód. Metóda má nasledujúcu štruktúru:

[atribúty] [špecifikátory] názov typu ([parametre])

Telo metódy;

Atribúty sú špeciálne inštrukcie pre kompilátor o vlastnostiach metódy. Atribúty sa používajú zriedka.

Kvalifikátory sú kľúčové slová, ktoré slúžia na rôzne účely, napríklad:

· Určenie dostupnosti metódy pre iné triedy:

o súkromné– metóda bude dostupná len v rámci tejto triedy

o chránené– metóda bude dostupná aj detským triedam

o verejnosti– metóda bude dostupná pre akúkoľvek inú triedu, ktorá má prístup k tejto triede

Označenie dostupnosti metódy bez vytvorenia triedy

· Typ nastavenia

Typ určuje výsledok, ktorý metóda vráti: môže to byť akýkoľvek typ dostupný v C#, ako aj kľúčové slovo void, ak sa výsledok nevyžaduje.

Názov metódy je identifikátor, ktorý sa použije na volanie metódy. Na identifikátor sa vzťahujú rovnaké požiadavky ako na názvy premenných: môže pozostávať z písmen, číslic a podčiarkovníka, ale nemôže začínať číslom.

Parametre sú zoznamom premenných, ktoré je možné pri volaní odovzdať metóde. Každý parameter pozostáva z typu a názvu premennej. Parametre sú oddelené čiarkami.

Telo metódy je normálny programový kód, okrem toho, že nemôže obsahovať definície iných metód, tried, menných priestorov atď. Ak metóda musí vrátiť nejaký výsledok, potom kľúčové slovo return musí byť prítomné na konci s návratovou hodnotou. . Ak vrátenie výsledkov nie je potrebné, potom nie je potrebné použiť kľúčové slovo return, hoci je povolené.

Príklad metódy, ktorá vyhodnocuje výraz:

verejný dvojitý Calc (dvojité a, dvojité b, dvojité c)

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

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

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

Preťaženie metódy

Jazyk C# vám umožňuje vytvárať viaceré metódy s rovnakými názvami, ale rôznymi parametrami. Kompilátor pri zostavovaní programu automaticky vyberie najvhodnejšiu metódu. Môžete napríklad napísať dve samostatné metódy na zvýšenie čísla na mocninu: jeden algoritmus by sa použil pre celé čísla a druhý by sa použil pre reálne čísla:

///

/// Vypočítajte X na mocninu Y pre celé čísla

///

private int Pow(int X, int Y)

///

/// Vypočítajte X na mocninu Y pre reálne čísla

///

súkromný dvojitý Pow (dvojité X, dvojité Y)

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

inak ak (Y == 0)

Tento kód sa volá rovnakým spôsobom, rozdiel je len v parametroch - v prvom prípade kompilátor zavolá metódu Pow s celočíselnými parametrami a v druhom - so skutočnými parametrami:

Predvolené nastavenia

Jazyk C# od verzie 4.0 (Visual Studio 2010) umožňuje nastaviť predvolené hodnoty niektorých parametrov, takže pri volaní metódy môžete niektoré parametre vynechať. Aby ste to dosiahli, pri implementácii metódy by sa mala požadovaným parametrom priradiť hodnota priamo v zozname parametrov:

private void GetData(int Číslo, int Voliteľné = 5 )

Console.WriteLine("Číslo: (0)", Číslo);

Console.WriteLine("Voliteľné: (0)", Voliteľné);

V tomto prípade môžete metódu zavolať takto:

GetData(10, 20);

V prvom prípade sa voliteľný parameter bude rovnať 20, pretože je explicitne špecifikovaný, a v druhom prípade sa bude rovnať 5, pretože nie je explicitne špecifikovaná a kompilátor má predvolenú hodnotu.

Predvolené parametre je možné nastaviť iba na pravej strane zoznamu parametrov, napríklad takýto podpis metódy nebude akceptovaný kompilátorom:

private void GetData (int Voliteľné = 5 , int Číslo)

Keď sú parametre odovzdávané metóde normálnym spôsobom (bez ďalších kľúčových slov ref a out), žiadne zmeny parametrov v rámci metódy neovplyvnia jej hodnotu v hlavnom programe. Povedzme, že máme nasledujúcu metódu:

private void Calc(int Number)

Je vidieť, že vo vnútri metódy sa mení premenná Number, ktorá bola odovzdaná ako parameter. Skúsme zavolať metódu:

Console.WriteLine(n);

Na obrazovke sa objaví číslo 1, to znamená, že napriek zmene premennej v metóde Calc sa hodnota premennej v hlavnom programe nezmenila. Je to spôsobené tým, že pri volaní metódy a kopírovať odovzdaná premenná, je to práve táto premenná, ktorú metóda mení. Keď sa metóda ukončí, hodnota kópií sa stratí. Tento spôsob odovzdávania parametra sa nazýva prejsť hodnotou.

Aby metóda zmenila premennú, ktorá jej bola odovzdaná, musí byť odovzdaná s kľúčovým slovom ref – musí byť v podpise metódy aj pri volaní:

private void Calc(ref int Number)

Console.WriteLine(n);

V tomto prípade sa na obrazovke objaví číslo 10: zmena hodnoty v metóde ovplyvnila aj hlavný program. Táto metóda prenosu sa nazýva prechádzanie odkazom, t.j. Už to nie je kópia, ktorá sa prenáša, ale odkaz na skutočnú premennú v pamäti.

Ak metóda používa premenné len na základe odkazu na vrátenie hodnôt a nezáleží jej na tom, čo v nich bolo pôvodne, nemôžete takéto premenné inicializovať, ale odovzdať ich s kľúčovým slovom out. Kompilátor chápe, že počiatočná hodnota premennej nie je dôležitá a nesťažuje sa na nedostatok inicializácie:

private void Calc (out int Number)

int n; // Nič neprideľujeme!

dátový typ reťazca

Jazyk C# používa na ukladanie reťazcov typ reťazca. Ak chcete deklarovať (a spravidla okamžite inicializovať) reťazcovú premennú, môžete napísať nasledujúci kód:

reťazec a = "Text";

reťazec b = "reťazce";

Môžete vykonať operáciu sčítania na riadkoch - v tomto prípade sa text jedného riadku pridá k textu druhého:

reťazec c = a + " " + b; // Výsledok: Text reťazca

Typ string je vlastne alias pre triedu String, ktorá umožňuje vykonávať množstvo zložitejších operácií s reťazcami. Napríklad metóda IndexOf môže vyhľadať podreťazec v reťazci a metóda Substring vráti časť reťazca zadanej dĺžky počnúc od zadanej pozície:

reťazec a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Výsledok: 14 (počítané od 0)

reťazec b = a.Podreťazec(3, 5); // Výsledok: DEFGH

Ak potrebujete do reťazca pridať špeciálne znaky, môžete to urobiť pomocou sekvencií escape začínajúcich spätnou lomkou:

Komponent ListBox

Komponent ListBox je zoznam, ktorého prvky sa vyberajú pomocou klávesnice alebo myši. Zoznam prvkov je určený vlastnosťou Položky. Položky sú prvkom, ktorý má svoje vlastnosti a svoje metódy. Metódy Pridať, RemoveAt A Vložiť sa používajú na pridávanie, mazanie a vkladanie prvkov.

Objekt Položky ukladá objekty do zoznamu. Objektom môže byť ľubovoľná trieda – dáta triedy sa prevedú na zobrazenie do reťazcovej reprezentácie metódou ToString. V našom prípade budú reťazce fungovať ako objekty. Keďže však objekt Items ukladá objekty pretypované na typ objektu, pred jeho použitím ich musíte pretypovať späť na ich pôvodný typ, v našom prípade reťazec:

reťazec a = (reťazec)zoznamBox1.Položky;

Na určenie čísla vybraného prvku použite vlastnosť SelectedIndex.

Keď som začal programovať v C++ a intenzívne som študoval knihy a články, vždy som narazil na tú istú radu: ak potrebujeme odovzdať nejaký objekt funkcii, ktorá by sa vo funkcii nemala meniť, vždy by sa mala odovzdať. odkazom na konštantu(PPSK), okrem tých prípadov, keď potrebujeme odovzdať buď primitívny typ, alebo im veľkosťou podobnú štruktúru. Pretože Za viac ako 10 rokov programovania v C++ som sa s touto radou stretával veľmi často (a sám som ju dal viackrát), už dávno sa do mňa „vstrebal“ – všetky argumenty automaticky odovzdávam odkazom na konštantu . Ale čas plynie a už prešlo 7 rokov, čo sme mali k dispozícii C++11 s jeho move sémantikou, v súvislosti s ktorou počúvam čoraz viac hlasov spochybňujúcich starú dobrú dogmu. Mnohí začínajú tvrdiť, že prechod podľa konštanty je minulosťou a teraz je to potrebné prejsť hodnotou(PPZ). Čo je za týmito rozhovormi, ako aj to, aké závery z toho všetkého môžeme vyvodiť, chcem rozobrať v tomto článku.

Knižná múdrosť

Aby sme pochopili, aké pravidlo by sme mali dodržiavať, navrhujem obrátiť sa na knihy. Knihy sú výborným zdrojom informácií, ktoré nie sme povinní prijímať, ale ktoré sa určite oplatí vypočuť. A začneme históriou, pôvodom. Nebudem zisťovať, kto bol prvým apologétom PPSC, jednoducho uvediem ako príklad knihu, ktorá na mňa osobne mala najväčší vplyv v otázke používania PPSC.

Mayers

Dobre, tu máme triedu, v ktorej sa všetky parametre odovzdávajú odkazom, sú s touto triedou nejaké problémy? Bohužiaľ existuje a tento problém leží na povrchu. V našej triede máme 2 funkčné entity: prvá nadobúda hodnotu vo fáze vytvárania objektu a druhá vám umožňuje zmeniť predtým nastavenú hodnotu. Máme dve entity, ale štyri funkcie. Teraz si predstavte, že môžeme mať nie 2 podobné entity, ale 3, 5, 6, čo potom? Potom budeme čeliť vážnemu nafúknutiu kódu. Preto, aby sa nevytvorilo množstvo funkcií, bol návrh úplne opustiť prepojenia v parametroch:

Šablóna Držiteľ triedy ( public: explicitný Držiteľ (hodnota T): m_Value(pohyb(hodnota)) ( ) void setValue(hodnotaT) (​m_Value = pohyb(hodnota); ) const T& hodnota() const noexcept ( návrat m_Hodnota; ) private: T m_Value; );

Prvá výhoda, ktorá vás hneď upúta, je podstatne menej kódu. Je ho ešte menej ako v úplne prvej verzii, kvôli odstráneniu const a & (aj keď pridali move ). Ale vždy nás učili, že prechádzať odkazom je produktívnejšie ako prechádzať hodnotou! Takto to bolo pred C++11 a ako to stále je, ale keď sa teraz pozrieme na tento kód, uvidíme, že sa tu nekopíruje viac ako v prvej verzii, za predpokladu, že T má konštruktor pohybu. Tie. Samotné PPSC bolo a bude rýchlejšie ako PPZ, ale kód nejako používa odovzdanú referenciu a často sa tento argument skopíruje.

Toto však nie je celý príbeh. Na rozdiel od prvej možnosti, kde máme len kopírovanie, tu pridávame aj pohyb. Ale sťahovanie je lacná operácia, však? Na túto tému má Mayersova kniha, o ktorej uvažujeme, aj kapitolu („Položka 29“), ktorá má názov: „Predpokladajme, že nie sú k dispozícii presuny, nie sú lacné a nepoužívajú sa.“ Hlavná myšlienka by mala byť jasná z názvu, ale ak chcete podrobnosti, určite si to prečítajte - nebudem sa tým zaoberať.

Tu by bolo vhodné vykonať úplnú komparatívnu analýzu prvej a poslednej metódy, ale nerád by som odbočoval od knihy, takže analýzu odložíme na ďalšie časti a tu budeme pokračovať v zvažovaní Scottových argumentov. Takže okrem skutočnosti, že tretia možnosť je zjavne kratšia ako druhá, v čom Scott vidí výhodu PPZ oproti PPSC v modernom kóde?

Vidí to v tom, že v prípade odovzdania rvalue, t.j. niektorí volajú takto: Držiak držiteľ(string("ja")); , možnosť s PPSC nám poskytne kopírovanie a možnosť s PPZ nám poskytne pohyb. Na druhej strane, ak je prevod takýto: Držiteľ držiteľa (nejakáLhodnota); , potom PPZ definitívne prehráva z dôvodu, že vykoná kopírovanie aj presúvanie, pričom vo verzii s PPSC bude kopírovanie len jedno. Tie. ukazuje sa, že PPZ, ak uvažujeme čisto o efektívnosti, je akýmsi kompromisom medzi množstvom kódu a „plnou“ (via && ) podporou sémantiky pohybu.

Preto Scott formuloval svoje rady tak starostlivo a tak starostlivo ich propaguje. Dokonca sa mi zdalo, že to nastolil s nevôľou, akoby pod tlakom: nemohol si pomôcť a neumiestnil do knihy diskusie na túto tému, lebo... diskutovalo sa o tom dosť široko a Scott bol vždy zberateľom kolektívnych skúseností. Okrem toho uvádza veľmi málo argumentov na obranu PPZ, ale uvádza veľa tých, ktoré túto „techniku“ spochybňujú. Na jeho argumenty proti sa pozrieme v neskorších častiach, ale tu stručne zopakujeme argument, ktorý Scott uvádza na obranu PPP (mentálne dodáva „ak objekt podporuje pohyb a je lacný“): umožňuje vyhnúť sa kopírovaniu pri odovzdávaní výrazu rvalue ako argumentu funkcie. Ale dosť trápenia Meyersovej knihy, prejdime k ďalšej knihe.

Mimochodom, ak niekto čítal knihu a je prekvapený, že tu neuvádzam možnosť s tým, čo Mayers nazval univerzálne referencie – teraz známe ako referencie preposielania – potom sa to dá ľahko vysvetliť. Uvažujem len o PPZ a PPSC, pretože... Považujem za zlú formu zavádzať šablónové funkcie pre metódy, ktoré nie sú šablónami len z dôvodu podpory prechodu odkazom oboch typov (rvalue/lvalue). Nehovoriac o tom, že kód sa ukazuje inak (už žiadna nemennosť) a prináša so sebou ďalšie problémy.

Josattis a spol

Posledná kniha, na ktorú sa pozrieme, je „C++ Templates“, ktorá je zároveň najnovšou zo všetkých kníh spomenutých v tomto článku. Vyšla koncom roka 2017 (a rok 2018 je uvedený vo vnútri knihy). Na rozdiel od iných kníh je táto úplne venovaná vzorom a nie radám (ako Mayers) alebo C++ vo všeobecnosti, ako Stroustrup. Preto sa tu zvažujú klady/zápory z pohľadu šablón na písanie.

Tejto téme je venovaná celá kapitola 7, ktorá má výrečný názov „Podľa hodnoty alebo podľa odkazu?“. V tejto kapitole autori pomerne stručne, ale výstižne popisujú všetky spôsoby prenosu so všetkými ich pre a proti. Analýza efektívnosti sa tu prakticky neuvádza a považuje sa za samozrejmé, že PPSC bude rýchlejší ako PPZ. Ale s týmto všetkým autori na konci kapitoly odporúčajú použiť predvolený PPP pre funkcie šablón. prečo? Pretože pomocou odkazu sa parametre šablóny zobrazia úplne a bez odkazu sa „rozpadnú“, čo má priaznivý vplyv na spracovanie polí a reťazcových literálov. Autori sa domnievajú, že ak sa to pre niektorý typ PPP ukáže ako neúčinné, potom môžete vždy použiť std::ref a std::cref . Toto je niekoľko rád, aby som bol úprimný, videli ste veľa ľudí, ktorí chcú používať vyššie uvedené funkcie?

Čo radia v súvislosti s PPSC? Odporúčajú používať PPSC, keď je výkon kritický alebo existuje niečo iné závažná dôvody nepoužívať PPP. Samozrejme, hovoríme tu len o štandardnom kóde, ale táto rada je v priamom rozpore so všetkým, čo sa programátori učili už desaťročie. Toto nie je len rada zvážiť PPP ako alternatívu – nie, toto je rada urobiť z PPSC alternatívu.

Týmto končíme naše knižné turné, pretože... Neviem o žiadnych iných knihách, ktoré by sme mali o tejto problematike konzultovať. Prejdime do iného mediálneho priestoru.

Sieťová múdrosť

Pretože Žijeme v dobe internetu, potom by ste sa nemali spoliehať len na knižnú múdrosť. Navyše mnohí autori, ktorí predtým písali knihy, teraz jednoducho píšu blogy a knihy opustili. Jedným z týchto autorov je Herb Sutter, ktorý v máji 2013 publikoval na svojom blogu „GotW #4 Solution: Class Mechanics“ článok, ktorý sa ho aj napriek tomu, že nie je úplne venovaný problému, ktorým sa zaoberáme, dotýka.

Takže v pôvodnej verzii článku Sutter jednoducho zopakoval starú múdrosť: „odovzdať parametre odkazom na konštantu“, ale túto verziu článku už neuvidíme, pretože Článok obsahuje opačnú radu: „ Ak parameter sa stále skopíruje a potom ho odovzdajte podľa hodnoty." Opäť notoricky známe „keby“. Prečo Sutter zmenil článok a ako som sa o tom dozvedel? Z komentárov. Prečítajte si komentáre k jeho článku, mimochodom, sú zaujímavejšie a užitočnejšie ako samotný článok. Pravda, Sutter po napísaní článku konečne zmenil názor a už takéto rady nedáva. Zmenu názoru možno nájsť v jeho prejave na CppCon v roku 2014: „Späť k základom! Základy moderného štýlu C++“. Určite sa pozrite, prejdeme na ďalší internetový odkaz.

A ďalej tu máme hlavný programovací zdroj 21. storočia: StackOverflow. Alebo skôr odpoveď, pričom počet pozitívnych reakcií v čase písania tohto článku presiahol 1700. Otázka znie: Aký je idióm kopírovania a výmeny? , a ako by názov mal napovedať, nie celkom na tému, na ktorú sa pozeráme. No v odpovedi na túto otázku sa autor dotýka aj témy, ktorá nás zaujíma. Odporúča tiež používať PPZ „ak bude argument aj tak skopírovaný“ (je čas zaviesť skratku aj pre toto, preboha). A vo všeobecnosti sa táto rada javí ako celkom vhodná, v rámci jeho odpovede a tam diskutovaného operátora=, ale autor si dovoľuje dať takúto radu aj v širšom zmysle a nielen v tomto konkrétnom prípade. Okrem toho ide ďalej ako všetky tipy, o ktorých sme predtým diskutovali, a žiada to urobiť aj v kóde C++03! Čo viedlo autora k takýmto záverom?

Autor odpovede zrejme čerpal hlavnú inšpiráciu z článku iného autora kníh a vývojára Boost.MPL na čiastočný úväzok – Davea Abrahamsa. Článok má názov „Chcete rýchlosť? Prejdite okolo hodnoty.“ , a to bolo uverejnené ešte v auguste 2009, t.j. 2 roky pred prijatím C++11 a zavedením sémantiky pohybu. Rovnako ako v predchádzajúcich prípadoch odporúčam, aby si čitateľ prečítal článok sám, ale uvediem hlavné argumenty (v skutočnosti existuje len jeden argument), ktoré Dave uvádza v prospech PPZ: musíte použiť PPZ , pretože s ním dobre funguje optimalizácia „skip copy“ ( copy elision ), ktorá v PPSC chýba. Ak si prečítate komentáre k článku, môžete vidieť, že rady, ktoré propaguje, nie sú univerzálne, čo potvrdzuje aj samotný autor, keď reaguje na kritiku od komentátorov. Článok však obsahuje výslovnú radu (usmernenie) použiť PPP, ak sa argument aj tak skopíruje. Mimochodom, ak by to niekoho zaujímalo, môžete si prečítať článok “Chcete rýchlosť? Neprekračujte (vždy) hodnotu.“ . Ako už názov napovedá, tento článok je reakciou na Daveov článok, takže ak ste čítali prvý, určite si prečítajte aj tento!

Bohužiaľ (pre niekoho našťastie) takéto články a (ešte viac) populárne odpovede na populárnych stránkach vedú k masívnemu používaniu pochybných techník (triviálny príklad) jednoducho preto, že si to vyžaduje menej písania a stará dogma už nie je neotrasiteľná – Vždy sa môžete odvolať na „tú populárnu radu“, ak vás pritlačí k stene. Teraz vám navrhujem, aby ste sa oboznámili s tým, čo nám ponúkajú rôzne zdroje s odporúčaniami na písanie kódu.

Pretože Keďže rôzne normy a odporúčania sú teraz zverejnené aj online, rozhodol som sa klasifikovať túto časť ako „sieťovú múdrosť“. Tu by som teda rád hovoril o dvoch zdrojoch, ktorých účelom je zlepšiť kód programátorov v C++ tým, že im poskytneme tipy (smernice), ako napísať práve tento kód.

Prvý súbor pravidiel, ktoré by som chcel zvážiť, bol poslednou kvapkou, ktorá ma prinútila prijať tento článok. Táto súprava je súčasťou ušľachtilej pomôcky a neexistuje mimo nej. Rovnako ako všetko, čo súvisí s clangom, aj tento nástroj je veľmi populárny a už dostal integráciu s CLion a Resharper C++ (tak som na to narazil). Clang-tydy teda obsahuje pravidlo modernizácie prechodu podľa hodnoty, ktoré funguje na konštruktéroch, ktoré akceptujú argumenty cez PPSC. Toto pravidlo navrhuje, aby sme nahradili PPSC za PPZ. Navyše v čase písania článku popis tohto pravidla obsahuje poznámku, že toto pravidlo Zbohom funguje len pre konštruktérov, no tí (kto sú?) radi prijmú pomoc od tých, ktorí toto pravidlo rozšíria aj na ďalšie subjekty. V popise je odkaz na Daveov článok - je jasné, odkiaľ nohy pochádzajú.

Na záver, aby som uzavrel túto recenziu múdrosti a autoritatívnych názorov iných ľudí, navrhujem, aby ste si pozreli oficiálne pokyny pre písanie kódu C++: C++ Core Guidelines, ktorých hlavnými editormi sú Herb Sutter a Bjarne Stroustrup (nie je to zlé, však?). Tieto odporúčania teda obsahujú nasledovné pravidlo: „Pre parametre „in“ odovzdávajte lacno skopírované typy podľa hodnoty a ostatné podľa odkazu na const“, čím sa úplne opakuje stará múdrosť: PPSK všade a PPP pre malé objekty. Tento tip načrtáva niekoľko alternatív, ktoré je potrebné zvážiť. v prípade, že prechod argumentu potrebuje optimalizáciu. Ale PPZ nie je zaradený do zoznamu alternatív!

Keďže nemám žiadne iné zdroje hodné pozornosti, navrhujem prejsť k priamej analýze oboch spôsobov prenosu.

Analýza

Celý predchádzajúci text je napísaný pre mňa trochu nezvyčajným spôsobom: prezentujem názory iných ľudí a dokonca sa snažím nevyjadrovať svoje (viem, že to dopadne zle). Z veľkej časti kvôli tomu, že názory iných a mojím cieľom bolo urobiť ich stručný prehľad, som odložil podrobné zváženie určitých argumentov, ktoré som našiel u iných autorov. V tejto časti sa nebudem odvolávať na úrady a dávať stanoviská, tu sa pozrieme na niektoré objektívne výhody a nevýhody PPSC a PPZ, ktoré budú okorenené mojím subjektívnym vnímaním. Samozrejme, niektoré z toho, čo bolo prediskutované vyššie, sa zopakuje, ale bohužiaľ, toto je štruktúra tohto článku.

Má PPP výhodu?

Pred zvážením argumentov pre a proti teda navrhujem pozrieť sa na to, čo a v akých prípadoch nám dáva výhoda, ktorú nám dáva míňanie hodnoty. Povedzme, že máme takúto triedu:

Trieda CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByTerNotByCoverMoverA aluerAndNotMover = byVal uerAndNotMover; ) void setRvaluer (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Hoci nás pre účely tohto článku zaujímajú len prvé dve funkcie, zahrnul som štyri možnosti, len aby som ich použil ako kontrast.

Trieda Accounter je jednoduchá trieda, ktorá počíta, koľkokrát bola skopírovaná/presunutá. A v triede CopyMover sme implementovali funkcie, ktoré nám umožňujú zvážiť nasledujúce možnosti:

    sťahovanie prešiel argument.

    Prejdite podľa hodnoty a potom kopírovanie prešiel argument.

Ak teraz každej z týchto funkcií odovzdáme lvalue, napríklad takto:

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

potom dostaneme nasledujúce výsledky:

Jednoznačným víťazom je PPSC, pretože... dáva len jeden exemplár, kým PPZ dáva jeden exemplár a jeden ťah.

Teraz sa pokúsime odovzdať rvalue:

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

Získame nasledovné:

Nie je tu jasný víťaz, pretože... PPZ aj PPSK majú po jednej operácii, ale vzhľadom na to, že PPZ využíva pohyb a PPSK kopírovanie, môžeme dať víťazstvo PPZ.

Tým sa však naše experimenty nekončia; pridajme nasledujúce funkcie na simuláciu nepriameho volania (s následným odovzdaním argumentu):

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

Použijeme ich úplne rovnako ako bez nich, takže kód nebudem opakovať (v prípade potreby sa pozrite do úložiska). Takže pre lvalue budú výsledky takéto:

Všimnite si, že PPSC zväčšuje medzeru s PPZ a zostáva pri jedinej kópii, zatiaľ čo PPZ už má až 3 operácie (jeden ďalší pohyb)!

Teraz prejdeme rvalue a získame nasledujúce výsledky:

Teraz má PPZ 2 pohyby a PPSC má stále jednu kópiu. Je teraz možné nominovať PPZ za víťaza? Nie, pretože ak by jeden ťah nemal byť horší ako jedna kópia, nemôžeme to isté povedať o 2 ťahoch. Preto v tomto príklade nebude víťaz.

Môžu mi namietať: „Autor, máš zaujatý názor a ťaháš to, čo je pre teba výhodné. Aj 2 ťahy budú lacnejšie ako kopírovanie!“ S týmto tvrdením nemôžem súhlasiť Všetko vo všetkom, pretože O koľko rýchlejšie je presúvanie ako kopírovanie závisí od konkrétnej triedy, no na „lacné“ presúvanie sa pozrieme v samostatnej časti.

Tu sme sa dotkli zaujímavej veci: pridali sme jeden nepriamy hovor a PPP pridal presne jednu operáciu v „váhu“. Myslím, že nemusíte mať diplom z MSTU, aby ste pochopili, že čím viac nepriamych hovorov máme, tým viac operácií sa vykoná pri použití PPZ, zatiaľ čo pre PPSC zostane číslo nezmenené.

Je nepravdepodobné, že by všetko, o čom sme diskutovali vyššie, bolo pre niekoho zjavením, možno sme ani nevykonali experimenty – všetky tieto čísla by mali byť väčšine programátorov v C++ zrejmé na prvý pohľad. Pravda, ešte jeden bod si zaslúži objasnenie: prečo v prípade rvalue nemá PZ kópiu (alebo iný ťah), ale len jeden ťah.

Nuž, pozreli sme sa na rozdiel v prenose medzi PPZ a PPSC tak, že sme z prvej ruky sledovali počet kópií a ťahov. Aj keď je zrejmé, že výhoda PPZ oproti PPSC aj v takýchto jednoduchých príkladoch je, mierne povedané nie Je zrejmé, že stále robím, trochu prezieravo, nasledujúci záver: ak budeme stále kopírovať argument funkcie, potom má zmysel zvážiť odovzdanie argumentu funkcii hodnotou. Prečo som dospel k tomuto záveru? Ak chcete plynulo prejsť na ďalšiu časť.

Ak skopírujeme...

Dostávame sa teda k povestnému „keby“. Väčšina argumentov, s ktorými sme sa stretli, nevyžadovala univerzálnu implementáciu PPP namiesto PPSC; požadovali to iba „ak sa argument aj tak skopíruje“. Je čas zistiť, čo je na tomto argumente zlé.

Chcem začať malým popisom toho, ako píšem kód. V poslednom čase sa môj proces kódovania čoraz viac podobá na TDD, t.j. zápis akejkoľvek metódy triedy začína napísaním testu, v ktorom sa táto metóda objaví. Preto, keď začínam písať test a vytváram metódu po napísaní testu, stále neviem, či skopírujem argument. Samozrejme, nie všetky funkcie sú vytvorené týmto spôsobom, často aj v procese písania testu presne viete, aká bude implementácia. Ale nie vždy sa to stane!

Niekto by mi mohol namietať, že je úplne jedno, ako bola metóda pôvodne napísaná, môžeme zmeniť spôsob, akým odovzdávame argument, keď metóda nadobudla tvar a je nám úplne jasné, čo sa tam deje (t.j. či máme kopírovanie resp. nie). Čiastočne s tým súhlasím - skutočne, môžete to urobiť týmto spôsobom, ale toto nás zapája do akejsi podivnej hry, kde musíme meniť rozhrania len preto, že sa zmenila implementácia. Čo nás privádza k ďalšej dileme.

Ukazuje sa, že rozhranie upravujeme (alebo dokonca plánujeme) podľa toho, ako bude implementované. Nepovažujem sa za odborníka na OOP a iné teoretické výpočty softvérovej architektúry, ale takéto akcie jasne odporujú základným pravidlám, kedy by implementácia nemala ovplyvniť rozhranie. Samozrejme, určité detaily implementácie (či už sú to vlastnosti jazyka alebo cieľovej platformy) stále prenikajú cez rozhranie tak či onak, ale mali by ste sa snažiť počet takýchto vecí znižovať, nie zvyšovať.

Nuž, Boh mu žehnaj, choďme touto cestou a stále zmeňme rozhrania v závislosti od toho, čo robíme pri implementácii v zmysle kopírovania argumentu. Povedzme, že sme napísali túto metódu:

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

a odovzdali naše zmeny do úložiska. Postupom času náš softvérový produkt nadobudol novú funkcionalitu, integrovali sa nové frameworky a vyvstala úloha informovať vonkajší svet o zmenách v našej triede. Tie. Do našej metódy pridáme nejaký oznamovací mechanizmus, nech je to niečo podobné signálom Qt:

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

Je problém s týmto kódom? Jedzte. Pre každé volanie setName posielame signál, takže signál bude odoslaný aj vtedy význam m_Name sa nezmenilo. Okrem problémov s výkonom môže táto situácia viesť k nekonečnej slučke v dôsledku toho, že kód, ktorý dostane vyššie uvedené upozornenie, nejako začne volať setName . Aby sa predišlo všetkým týmto problémom, takéto metódy najčastejšie vyzerajú takto:

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

Zbavili sme sa problémov popísaných vyššie, ale teraz zlyhalo naše pravidlo „ak aj tak skopírujeme...“ – už nie je bezpodmienečné kopírovanie argumentu, teraz ho skopírujeme, len ak sa zmení! Čo by sme teda teraz mali robiť? Zmeniť rozhranie? Dobre, poďme zmeniť rozhranie triedy kvôli tejto oprave. Čo ak naša trieda zdedila túto metódu z nejakého abstraktného rozhrania? Zmeňme to aj tam! Je veľa zmien, pretože sa zmenila implementácia?

Opäť môžu namietať proti mne, hovoria, autor, prečo sa snažíte šetriť peniaze na zápalky, keď tam táto podmienka vyjde? Áno, väčšina hovorov bude falošná! Je v tom nejaká dôvera? Kde? A ak som sa rozhodol ušetriť na zápasoch, nebolo práve to, že sme použili PPZ, dôsledkom práve takýchto úspor? Len pokračujem v „straníckej línii“, ktorá obhajuje efektivitu.

Konštruktéri

Poďme si v krátkosti prejsť konštruktory, najmä preto, že pre nich v clang-tidy existuje špeciálne pravidlo, ktoré zatiaľ nefunguje pre iné metódy/funkcie. Povedzme, že máme takúto triedu:

Trieda JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Je zrejmé, že parameter sa skopíruje a clang-tidy nám povie, že by bolo dobré prepísať konštruktor na toto:

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

A úprimne povedané, je pre mňa ťažké tu argumentovať - ​​koniec koncov, naozaj vždy kopírujeme. A najčastejšie, keď niečo prejdeme cez konštruktor, tak to skopírujeme. Ale častejšie neznamená vždy. Tu je ďalší príklad:

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

Tu nekopírujeme vždy, ale iba vtedy, keď sú dátumy uvedené správne. Samozrejme, v drvivej väčšine prípadov to tak bude. ale nie vždy.

Môžete uviesť ďalší príklad, ale tentoraz bez kódu. Predstavte si, že máte triedu, ktorá prijíma veľký objekt. Trieda existuje už dlho a teraz je čas aktualizovať jej implementáciu. Uvedomujeme si, že nepotrebujeme viac ako polovicu veľkého zariadenia (ktoré sa rokmi rozrástlo) a možno ešte menej. Môžeme s tým niečo urobiť tým, že prejdeme okolo hodnoty? Nie, nebudeme môcť nič urobiť, pretože sa vytvorí kópia. Ak by sme však používali PPSC, jednoducho by sme zmenili to, čo robíme vnútri dizajnér. A toto je kľúčový bod: pomocou PPSC kontrolujeme, čo a kedy sa stane pri implementácii našej funkcie (konštruktora), ale ak použijeme PPZ, stratíme akúkoľvek kontrolu nad kopírovaním.

Čo si z tejto sekcie odniesť? Skutočnosť, že argument „ak budeme kopírovať...“ je veľmi kontroverzný, pretože Nie vždy vieme, čo budeme kopírovať, a aj keď to vieme, veľmi často si nie sme istí, že to bude pokračovať aj v budúcnosti.

Sťahovanie je lacné

Od chvíle, keď sa objavila sémantika pohybu, začala mať vážny vplyv na spôsob písania moderného kódu C++ a postupom času sa tento vplyv len zintenzívnil: niet divu, pretože pohyb je taký lacno v porovnaní s kopírovaním. Ale je to tak? Je pravda, že pohyb je Vždy lacná operácia? Toto sa pokúsime zistiť v tejto časti.

Binárny veľký objekt

Začnime s triviálnym príkladom, povedzme, že máme nasledujúcu triedu:

Struct Blob ( std::array údaje; );

Obyčajný kvapka(BDO, anglicky BLOB), ktoré možno použiť v rôznych situáciách. Pozrime sa, koľko nás bude stáť prechod podľa referencie a podľa hodnoty. Naše BDO sa použije asi takto:

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

A tieto funkcie budeme nazývať takto:

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

Kód pre ďalšie príklady bude identický s týmto, iba s inými názvami a typmi, takže pre zostávajúce príklady ho neuvediem - všetko je v úložisku.

Než prejdeme k meraniam, skúsme predpovedať výsledok. Takže máme 4 KB std::array, ktoré chceme uložiť do objektu triedy Storage. Ako sme už skôr zistili, pre PPSC budeme mať jednu kópiu, zatiaľ čo pre PPZ budeme mať jednu kópiu a jeden ťah. Na základe skutočnosti, že pole nie je možné presunúť, budú 2 kópie pre PPZ oproti jednej pre PPSC. Tie. môžeme očakávať dvojnásobnú prevahu vo výkone pre PPSC.

Teraz sa pozrime na výsledky testu:

Tento a všetky nasledujúce testy boli spustené na rovnakom počítači pomocou MSVS 2017 (15.7.2) a príznaku /O2.

Prax sa zhodovala s predpokladom - prechádzanie hodnotou je 2-krát drahšie, pretože pre pole je presun úplne ekvivalentný kopírovaniu.

Linka

Pozrime sa na ďalší príklad, obyčajný std::string . Čo môžeme očakávať? Vieme (o tom som hovoril v článku), že moderné implementácie rozlišujú dva typy reťazcov: krátky (okolo 16 znakov) a dlhý (tie, ktoré sú dlhšie ako krátke). Pre krátke sa používa interný buffer, čo je bežné C-pole char , no dlhé už budú umiestnené na halde. Krátke rady nás nezaujímajú, pretože... výsledok tam bude rovnaký ako pri BDO, takže sa zamerajme na dlhé rady.

Takže pri dlhej strune je zrejmé, že jej presunutie by malo byť celkom lacné (stačí pohnúť ukazovateľom), takže sa môžete spoľahnúť na to, že posunutie struny by vôbec nemalo ovplyvniť výsledky a PPZ by mal dať výsledok nie horšie ako PPSC. Pozrime sa na to v praxi a získame nasledujúce výsledky:

Prejdeme k vysvetleniu tohto „fenoménu“. Čo sa teda stane, keď skopírujeme existujúci reťazec do už existujúceho reťazca? Pozrime sa na triviálny príklad:

String first(64, "C"); string second(64, "N"); //... druhý = prvý;

Máme dva 64-znakové reťazce, takže vnútorná vyrovnávacia pamäť je pri ich vytváraní nedostatočná, výsledkom čoho sú oba reťazce alokované na halde. Teraz skopírujeme prvý na druhý. Pretože naše veľkosti riadkov sú rovnaké, je zrejmé, že v sekunde je dostatok miesta na umiestnenie všetkých údajov z prvého, takže druhý = prvý; bude banálny memcpy, nič viac. Ak sa však pozrieme na mierne upravený príklad:

String first(64, "C"); reťazec druhý = prvý;

potom už nebude volanie operátora=, ale bude sa volať konštruktor kopírovania. Pretože Keďže máme do činenia s konštruktorom, neexistuje v ňom žiadna existujúca pamäť. Najprv ho treba vybrať a až potom skopírovať. Tie. toto je pridelenie pamäte a potom memcpy. Ako vy a ja vieme, alokácia pamäte na globálnej halde je zvyčajne drahá operácia, takže kopírovanie z druhého príkladu bude drahšie ako kopírovanie z prvého. Drahšie na pridelenie pamäte haldy.

Čo to má spoločné s našou témou? Najpriamejší, pretože prvý príklad ukazuje presne to, čo sa stane s PPSC, a druhý ukazuje, čo sa stane s PPZ: pre PPZ sa vždy vytvorí nový riadok, zatiaľ čo pre PPSC sa znova použije existujúci. Rozdiel v dobe realizácie ste už videli, takže tu nie je čo dodať.

Aj tu sa stretávame s faktom, že pri využívaní PPP pracujeme mimo kontextu, a preto nemôžeme využiť všetky výhody, ktoré môže poskytnúť. A ak sme predtým uvažovali v zmysle teoretických budúcich zmien, tu pozorujeme veľmi konkrétne zlyhanie v produktivite.

Samozrejme, niekto by mi mohol namietať, že reťazec stojí oddelene a väčšina typov takto nefunguje. Na čo môžem odpovedať nasledovne: všetko popísané vyššie bude platiť pre každý kontajner, ktorý okamžite alokuje pamäť v halde pre balík prvkov. Tiež, kto vie, aké ďalšie kontextové optimalizácie sa používajú v iných typoch?

Čo by ste si z tejto sekcie mali odniesť? To, že aj keď je presúvanie naozaj lacné, neznamená, že nahradenie kopírovania kopírovaním+presúvaním vždy prinesie výkonovo porovnateľný výsledok.

Komplexný typ

Nakoniec sa pozrime na typ, ktorý bude pozostávať z viacerých objektov. Nech je to trieda Osoba, ktorá pozostáva z údajov vlastných osobe. Zvyčajne je to vaše meno, priezvisko, PSČ atď. Toto všetko môžete reprezentovať ako reťazce a predpokladať, že reťazce, ktoré vložíte do polí triedy Osoba, budú pravdepodobne krátke. Aj keď verím, že v reálnom živote bude najužitočnejšie meranie krátkych strún, aj tak sa pozrieme na struny rôznych veľkostí, aby sme poskytli ucelenejší obraz.

Použijem aj Osoba s 10 poliami, ale na to nevytvorím 10 polí priamo v tele triedy. Implementácia Person ukrýva kontajner vo svojich hĺbkach – vďaka tomu je pohodlnejšie meniť parametre testu prakticky bez toho, aby sa odchýlilo od toho, ako by to fungovalo, keby bol Person skutočnou triedou. Implementácia je však k dispozícii a vždy môžete skontrolovať kód a povedať mi, či som urobil niečo zle.

Takže poďme: Osoba s 10 poliami typu string , ktoré prenášame pomocou PPSC a PPZ do Úložiska :

Ako môžete vidieť, máme obrovský rozdiel vo výkone, čo by po predchádzajúcich častiach nemalo byť pre čitateľov prekvapením. Tiež sa domnievam, že trieda Osoba je dostatočne „skutočná“, takže takéto výsledky nebudú odmietnuté ako abstraktné.

Mimochodom, keď som pripravoval tento článok, pripravil som ďalší príklad: triedu, ktorá používa niekoľko objektov std::function. Podľa mojej predstavy to malo vykazovať aj výhodu vo výkone PPSC oproti PPZ, no dopadlo to presne naopak! Tento príklad tu však neuvádzam preto, že by sa mi nepáčili výsledky, ale preto, že som nemal čas zistiť, prečo sa také výsledky dosiahli. Napriek tomu je v úložisku kód (Tlačiarne), testy - tiež, ak by na to chcel niekto prísť, budem rád, ak sa dozviem o výsledkoch výskumu. K tomuto príkladu sa plánujem vrátiť neskôr a ak tieto výsledky nikto predo mnou nezverejní, potom sa im budem venovať v samostatnom článku.

Výsledky

Pozreli sme sa teda na rôzne výhody a nevýhody prechodu hodnotou a prechodu odkazom na konštantu. Pozreli sme sa na niekoľko príkladov a pozreli sme sa na výkonnosť oboch metód v týchto príkladoch. Tento článok samozrejme nemôže a nie je vyčerpávajúci, ale podľa môjho názoru obsahuje dostatok informácií na nezávislé a informované rozhodnutie o tom, ktorý spôsob je najlepšie použiť. Niekto môže namietať: "Prečo používať jednu metódu, začnime od úlohy!" Hoci s touto tézou vo všeobecnosti súhlasím, v tejto situácii s ňou nesúhlasím. Verím, že v jazyku môže byť len jeden spôsob podávania argumentov, ktorý sa štandardne používa.

Čo znamená predvolené? To znamená, že keď píšem funkciu, nemyslím na to, ako by som mal odovzdať argument, len použijem „default“. Jazyk C++ je pomerne zložitý jazyk, ktorému sa veľa ľudí vyhýba. A podľa mňa tá zložitosť nie je spôsobená ani tak zložitosťou jazykových konštruktov, ktoré v jazyku existujú (typický programátor sa s nimi nemusí nikdy stretnúť), ale tým, že jazyk vás núti veľa premýšľať: oslobodil som up memory, je drahé používať túto funkciu tu? a tak ďalej.

Mnohí programátori (C, C++ a iní) sú nedôverčiví a boja sa C++, ktoré sa začali objavovať po roku 2011. Počul som veľa kritiky, že jazyk sa stáva zložitejším, že v ňom teraz môžu písať iba „guruovia“ atď. Osobne sa domnievam, že to tak nie je – práve naopak, komisia venuje veľa času tomu, aby bol jazyk priateľskejší k začiatočníkom a aby programátori museli menej myslieť na vlastnosti jazyka. Koniec koncov, ak nemusíme bojovať s jazykom, potom máme čas premýšľať o úlohe. Tieto zjednodušenia zahŕňajú inteligentné ukazovatele, funkcie lambda a oveľa viac, ktoré sa objavili v jazyku. Zároveň nepopieram, že teraz treba viac študovať, ale čo je na štúdiu zlé? Alebo sa nedejú žiadne zmeny v iných populárnych jazykoch, ktoré je potrebné sa naučiť?

Ďalej nepochybujem o tom, že sa nájdu snobi, ktorí budú vedieť odpovedať: „Nechceš rozmýšľať? Potom píšte v PHP." Takýmto ľuďom sa ani nechcem zodpovedať. Uvediem len príklad z hernej reality: v prvej časti Starcraftu, keď sa v budove vytvorí nový robotník, aby mohol začať ťažiť nerasty (alebo plyn), ho tam museli manuálne poslať. Navyše, každé balenie minerálov malo limit, po dosiahnutí ktorého bol nárast pracovníkov zbytočný a dokonca sa mohli navzájom rušiť a zhoršovať produkciu. V Starcraft 2 sa to zmenilo: pracovníci automaticky začnú ťažiť nerasty (alebo plyn) a tiež to ukazuje, koľko pracovníkov momentálne ťaží a koľko je limit tohto ložiska. To značne zjednodušilo interakciu hráča so základňou, čo mu umožnilo sústrediť sa na dôležitejšie aspekty hry: budovanie základne, hromadenie jednotiek a ničenie nepriateľa. Zdalo by sa, že je to len skvelá inovácia, ale čo sa začalo na internete! Ľudia (kto sú oni?) začali kričať, že hra bola „pokazená“ a „zabili Starcraft“. Je zrejmé, že takéto správy mohli pochádzať iba od „strážcov tajných vedomostí“ a „adeptov vysokej APM“, ktorí boli radi v nejakom „elitnom“ klube.

Takže, keď sa vrátim k našej téme, čím menej musím premýšľať o tom, ako napísať kód, tým viac času mám na premýšľanie o riešení okamžitého problému. Premýšľanie o tom, ktorú metódu by som mal použiť - PPSC alebo PPZ - ma ani o kúsok nepribližuje k vyriešeniu problému, takže jednoducho odmietam premýšľať o takýchto veciach a vyberiem si jednu možnosť: prejsť odkazom na konštantu. prečo? Pretože nevidím žiadne výhody pre PPP vo všeobecných prípadoch a špeciálne prípady je potrebné posudzovať samostatne.

Je to špeciálny prípad, len keď som si všimol, že v niektorom spôsobe sa PPSC ukáže ako úzke hrdlo a zmenou prevodovky na PPZ dosiahneme dôležité zvýšenie výkonu, neváham použiť PPZ. Ale štandardne budem používať PPSC ako v bežných funkciách, tak aj v konštruktoroch. A ak to bude možné, budem propagovať túto konkrétnu metódu všade, kde to bude možné. prečo? Pretože si myslím, že prax propagácie PPP je krutá, pretože väčšina programátorov nie je príliš znalá (či už v princípe, alebo sa jednoducho ešte nedostala do swingu) a jednoducho sa riadia radami. Navyše, ak existuje niekoľko protichodných rád, vyberú si tú, ktorá je jednoduchšia, a to vedie k pesimizmu v kóde jednoducho preto, že niekto niekde niečo počul. Ach áno, tento niekto môže poskytnúť aj odkaz na Abrahamsov článok, aby dokázal, že má pravdu. A potom sedíte, čítate kód a premýšľate: je fakt, že parameter sa tu odovzdáva hodnotou, pretože programátor, ktorý to napísal, pochádza z Java, len si prečítal veľa „inteligentných“ článkov, alebo je naozaj potrebné technická špecifikácia?

PPSC sa číta oveľa ľahšie: človek jasne pozná „dobrú formu“ C++ a ideme ďalej – pohľad nezostáva. Prax používania PPSC bola učená programátorom v C++ roky, aký je dôvod, prečo ju opustiť? To ma vedie k ďalšiemu záveru: ak rozhranie metódy používa PPP, mal by tam byť aj komentár, prečo je to tak. V ostatných prípadoch sa musí použiť PPSC. Samozrejme, existujú typy výnimiek, ale neuvádzam ich tu jednoducho preto, že sú implikované: string_view , initializer_list , rôzne iterátory atď. Ide však o výnimky, ktorých zoznam sa môže rozširovať v závislosti od toho, aké typy sú v projekte použité. Ale podstata zostáva rovnaká od C++ 98: štandardne vždy používame PPCS.

Pre std::string s najväčšou pravdepodobnosťou nebude rozdiel na malých reťazcoch, o tom si povieme neskôr.

Vopred sa ospravedlňujem za domýšľavú anotáciu o „umiestňovaní bodov“, ale musíme vás nejako nalákať na článok)) Z mojej strany sa pokúsim zabezpečiť, aby abstrakt stále spĺňal vaše očakávania.

Stručne o čom hovoríme

Každý to už vie, ale na začiatku vám pripomeniem, ako možno parametre metódy odovzdať v 1C. Môžu byť odovzdané „odkazom“ alebo „hodnotou“. V prvom prípade odovzdáme metóde rovnakú hodnotu ako v bode volania a v druhom jej kópiu.

V predvolenom nastavení sa v 1C argumenty odovzdávajú odkazom a zmeny parametra vo vnútri metódy budú viditeľné zvonka metódy. Tu ďalšie pochopenie otázky závisí od toho, čo presne chápete pod slovom „zmena parametra“. Takže to znamená opätovné pridelenie a nič viac. Okrem toho môže byť priradenie implicitné, napríklad volanie metódy platformy, ktorá vráti niečo vo výstupnom parametri.

Ale ak nechceme, aby sa náš parameter odovzdával odkazom, potom môžeme pred parametrom zadať kľúčové slovo Význam

Procedure ByValue(Parameter hodnoty) Parameter = 2; parameter EndProcedure = 1; ByValue(Parameter); Správa (Parameter); // vytlačí 1

Všetko funguje tak, ako bolo sľúbené - zmena (alebo skôr „nahradenie“) hodnoty parametra nezmení hodnotu mimo metódy.

No, v čom je vtip?

Zaujímavé momenty začínajú, keď ako parametre začneme pridávať nie primitívne typy (reťazce, čísla, dátumy atď.), ale objekty. Tu vstupujú do hry pojmy ako „plytké“ a „hlboké“ kópie objektu, ako aj ukazovatele (nie v jazyku C++, ale ako abstraktné rukoväte).

Pri odovzdávaní objektu (napríklad tabuľky hodnôt) odkazom odovzdávame samotnú hodnotu ukazovateľa (určitý úchyt), ktorý „drží“ objekt v pamäti platformy. Keď prejde hodnotou, platforma vytvorí kópiu tohto ukazovateľa.

Inými slovami, ak pri odovzdávaní objektu odkazom v metóde priradíme parametru hodnotu „Array“, potom v bode volania dostaneme pole. Opätovné priradenie hodnoty odovzdanej odkazom je viditeľné z miesta hovoru.

Procedure ProcessValue(Parameter) Parameter = Nové pole; EndProcedure Table = New ValueTable; ProcessValue(Tabuľka); Správa (Typ hodnoty (Tabuľka)); // vypíše pole

Ak prejdeme objekt podľa hodnoty, potom sa v bode volania naša tabuľka hodnôt nestratí.

Obsah a stav objektu

Pri prechode hodnotou sa neskopíruje celý objekt, ale iba jeho ukazovateľ. Inštancia objektu zostáva rovnaká. Nezáleží na tom, ako odovzdávate objekt, podľa odkazu alebo podľa hodnoty - vymazaním tabuľky hodnôt sa vymaže samotná tabuľka. Toto čistenie bude viditeľné všade, pretože... bol len jeden objekt a nezáležalo na tom, ako presne bol odovzdaný metóde.

Procedure ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Table.Add(); ProcessValue(Tabuľka); Správa(Tabuľka.Množstvo()); // vypíše 0

Pri odovzdávaní objektov metódam platforma pracuje s ukazovateľmi (podmienené, nie priame analógy z C++). Ak je objekt odovzdaný odkazom, potom môže byť pamäťová bunka virtuálneho stroja 1C, v ktorej objekt leží, prepísaná iným objektom. Ak je objekt odovzdaný hodnotou, ukazovateľ sa skopíruje a prepísanie objektu nevedie k prepísaniu miesta v pamäti pôvodným objektom.

Zároveň akákoľvek zmena štát objekt (čistenie, pridávanie vlastností atď.) mení samotný objekt a nemá vôbec nič spoločné s tým, ako a kam bol objekt prenesený. Stav inštancie objektu sa zmenil; môže na ňu byť veľa „odkazov“ a „hodnôt“, ale inštancia je vždy rovnaká. Odovzdaním objektu metóde nevytvoríme kópiu celého objektu.

A to je vždy pravda, okrem...

Interakcia klient-server

Platforma implementuje volania servera veľmi transparentne. Jednoducho zavoláme metódu a platforma pod kapotou serializuje (premení sa na reťazec) všetky parametre metódy, odovzdá ich serveru a potom vráti výstupné parametre späť klientovi, kde sa deserializujú a fungujú ako keby nikdy neboli na žiadnom serveri.

Ako viete, nie všetky objekty platformy sú serializovateľné. Toto je miesto, kde obmedzenie rastie: nie všetky objekty môžu byť odovzdané metóde servera z klienta. Ak miniete objekt, ktorý nemožno serializovať, platforma začne používať zlé slová.

  • Výslovné vyhlásenie o zámeroch programátora. Pri pohľade na podpis metódy môžete jasne povedať, ktoré parametre sú vstupné a ktoré výstupné. Tento kód sa ľahšie číta a udržiava
  • Aby bola zmena parametra „podľa referencie“ na serveri viditeľná na call pointe na klientovi, p Samotná platforma nevyhnutne vráti parametre odovzdané serveru prostredníctvom odkazu na klienta, aby sa zabezpečilo správanie opísané na začiatku článku. Ak parameter nie je potrebné vrátiť, dôjde k prekročeniu návštevnosti. Pre optimalizáciu výmeny dát by parametre, ktorých hodnoty na výstupe nepotrebujeme, mali byť označené slovom Value.

Druhý bod je tu pozoruhodný. Na optimalizáciu návštevnosti platforma nevráti klientovi hodnotu parametra, ak je parameter označený slovom Hodnota. To všetko je skvelé, ale vedie to k zaujímavému efektu.

Ako som už povedal, keď sa objekt prenesie na server, dôjde k serializácii, t.j. vykoná sa "hlboká" kópia objektu. A ak existuje slovo Význam objekt nebude cestovať zo servera späť ku klientovi. Pridáme tieto dve skutočnosti a dostaneme nasledovné:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Parameter hodnoty) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= New ListValues; List1.Add("ahoj"); Zoznam2 = Zoznam1.Kopírovať(); Zoznam3 = Zoznam1.Kopírovať(); // objekt sa úplne skopíruje, // prenesie na server a potom sa vráti. // vymazanie zoznamu je viditeľné v bode volania ByRef(List1); // objekt sa úplne skopíruje, // prenesie na server. Nevracia sa. // Vymazanie zoznamu NIE JE VIDITELNÉ v bode volania ByValue(List2); // skopíruje sa iba ukazovateľ objektu // vymazanie zoznamu je viditeľné v bode volania ByValueClient(List3); Správa(Zoznam1.Množstvo()); Správa(Zoznam2.Množstvo()); Správa(Zoznam3.Množstvo()); Koniec procedúry

Zhrnutie

V skratke sa to dá zhrnúť takto:

  • Odovzdávanie odkazom vám umožňuje „prepísať“ objekt úplne iným objektom
  • Odovzdanie hodnoty vám neumožňuje „prepísať“ objekt, ale zmeny vo vnútornom stave objektu budú viditeľné, pretože pracujeme s rovnakou inštanciou objektu
  • Pri volaní servera sa pracuje s RÔZNYMI inštanciami objektu, pretože Bola vykonaná hĺbková kópia. Kľúčové slovo Význam zabráni skopírovaniu inštancie servera späť do inštancie klienta a zmena interného stavu objektu na serveri nepovedie k podobnej zmene na klientovi.

Dúfam, že tento jednoduchý zoznam pravidiel vám uľahčí riešenie sporov s kolegami ohľadom odovzdávania parametrov „podľa hodnoty“ a „podľa referencie“