Mít hodnotu. Předávání parametrů odkazem a hodnotou. Výchozí nastavení

Nechť Factorial(n) je funkce pro výpočet faktoriálu čísla n. Poté, vzhledem k tomu, že „víme“ faktoriál 1 je 1, můžeme sestavit následující řetězec:

Faktor(4) = Faktor(3)*4

Faktor(3) = Faktor(2)*3

Faktor(2) = Faktor(1)*2

Pokud bychom ale neměli terminální podmínku, že když n=1 má faktoriální funkce vrátit 1, pak by takový teoretický řetězec nikdy neskončil a mohlo jít o chybu přetečení zásobníku volání – přetečení zásobníku volání. Abychom pochopili, co je zásobník volání a jak může přetékat, podívejme se na rekurzivní implementaci naší funkce:

Funkce factorial(n: Integer): LongInt;

Pokud n=1 Pak

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

Konec;

Jak vidíme, aby řetězec fungoval správně, před každým dalším voláním funkce na sebe je nutné všechny lokální proměnné někam uložit, aby při obrácení řetězce byl výsledek správný (vypočtená hodnota faktoriálu n-1 se vynásobí n ). V našem případě, pokaždé, když je faktoriál volán sám od sebe, musí být uloženy všechny hodnoty proměnné n. Oblast, ve které jsou uloženy lokální proměnné funkce při jejím rekurzivním volání, se nazývá zásobník volání. Tento zásobník samozřejmě není nekonečný a může být vyčerpán, pokud jsou rekurzivní volání konstruována nesprávně. Konečnost iterací našeho příkladu je zaručena tím, že při n=1 se volání funkce zastaví.

Předávání parametrů hodnotou a odkazem

Až dosud jsme nemohli změnit hodnotu v podprogramu skutečný parametr(tj. parametr, který je zadán při volání podprogramu) a v některých aplikačních úlohách by to bylo výhodné. Vzpomeňme na proceduru Val, která mění hodnotu dvou svých skutečných parametrů najednou: prvním je parametr, kam se bude zapisovat převedená hodnota řetězcové proměnné, a druhým je parametr Code, kde číslo chybného znak je umístěn v případě selhání při převodu typu. Tito. stále existuje mechanismus, kterým může podprogram měnit skutečné parametry. To je možné díky různým způsobům předávání parametrů. Pojďme se na tyto metody podívat blíže.

Programování v Pascalu

Předávání parametrů hodnotou

V podstatě jsme takto předali všechny parametry našim rutinám. Mechanismus je následující: když je specifikován skutečný parametr, jeho hodnota je zkopírována do oblasti paměti, kde je umístěn podprogram, a poté, co funkce nebo procedura dokončí svou práci, je tato oblast vymazána. Zhruba řečeno, když je podprogram spuštěn, existují dvě kopie jeho parametrů: jedna v rozsahu volajícího programu a druhá v rozsahu funkce.

Při této metodě předávání parametrů zabere volání podprogramu více času, protože kromě samotného volání je nutné zkopírovat všechny hodnoty všech skutečných parametrů. Pokud je podprogramu předáno velké množství dat (například pole s velkým počtem prvků), může být doba potřebná ke zkopírování dat do místní oblasti značná a je třeba to vzít v úvahu při vývoji programů a hledání úzkých míst v jejich výkonu.

Při tomto způsobu přenosu nemohou být podprogramem změněny skutečné parametry, protože změny ovlivní pouze izolovanou místní oblast, která bude uvolněna po dokončení funkce nebo procedury.

Předávání parametrů odkazem

Při této metodě se do podprogramu nezkopírují hodnoty skutečných parametrů, ale přenesou se adresy v paměti (odkazy na proměnné), kde se nacházejí. V tomto případě podprogram již mění hodnoty, které nejsou v místním rozsahu, takže všechny změny budou viditelné pro volající program.

K označení, že argument musí být předán odkazem, je před jeho deklaraci přidáno klíčové slovo var:

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

n1:=random(rozsah);

n2:=random(rozsah); konec ;

var rand1, rand2: celé číslo;

Začít getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Konec.

V tomto příkladu jsou odkazy na dvě proměnné předány proceduře getTwoRandom jako skutečné parametry: rand1 a rand2. Třetí aktuální parametr (10) je předán hodnotou. Procedura zapisuje pomocí formálních parametrů

Metody programování pomocí řetězců

Účel laboratorní práce : naučit se metody v jazyce C#, pravidla pro práci se znakovými daty a komponentu ListBox. Napište program pro práci s řetězci.

Metody

Metoda je prvek třídy, který obsahuje programový kód. Metoda má následující strukturu:

[atributy] [specifikátory] název typu ([parametry])

Tělo metody;

Atributy jsou speciální instrukce pro kompilátor o vlastnostech metody. Atributy se používají zřídka.

Kvalifikátory jsou klíčová slova, která slouží k různým účelům, například:

· Určení dostupnosti metody pro jiné třídy:

Ó soukromé– metoda bude dostupná pouze v rámci této třídy

Ó chráněný– metoda bude dostupná i dětským třídám

Ó veřejnost– metoda bude dostupná jakékoli jiné třídě, která má k této třídě přístup

Označení dostupnosti metody bez vytvoření třídy

· Typ nastavení

Typ určuje výsledek, který metoda vrátí: může to být jakýkoli typ dostupný v C#, stejně jako klíčové slovo void, pokud výsledek není vyžadován.

Název metody je identifikátor, který bude použit k volání metody. Na identifikátor se vztahují stejné požadavky jako na názvy proměnných: může se skládat z písmen, číslic a podtržítka, ale nemůže začínat číslem.

Parametry jsou seznam proměnných, které lze při volání předat metodě. Každý parametr se skládá z typu proměnné a názvu. Parametry jsou odděleny čárkami.

Tělo metody je normální programový kód, kromě toho, že nemůže obsahovat definice jiných metod, tříd, jmenných prostorů atd. Pokud metoda musí vrátit nějaký výsledek, pak klíčové slovo return musí být přítomno na konci s návratovou hodnotou. . Pokud vracení výsledků není nutné, pak použití klíčového slova return není nutné, i když je povoleno.

Příklad metody, která vyhodnocuje výraz:

veřejný 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);

Přetížení metody

Jazyk C# umožňuje vytvářet více metod se stejnými názvy, ale různými parametry. Překladač automaticky vybere nejvhodnější metodu při sestavování programu. Například byste mohli napsat dvě samostatné metody pro zvýšení čísla na mocninu: jeden algoritmus by byl použit pro celá čísla a jiný by byl použit pro reálná čísla:

///

/// Vypočítejte X na mocninu Y pro celá čísla

///

private int Pow(int X, int Y)

///

/// Vypočítejte X na mocninu Y pro reálná čísla

///

soukromý dvojitý Pow (dvojité X, dvojité Y)

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

jinak if (Y == 0)

Tento kód se volá stejným způsobem, rozdíl je pouze v parametrech - v prvním případě kompilátor zavolá metodu Pow s celočíselnými parametry a ve druhém - se skutečnými parametry:

Výchozí nastavení

Jazyk C# počínaje verzí 4.0 (Visual Studio 2010) umožňuje nastavit výchozí hodnoty některých parametrů, takže při volání metody můžete některé parametry vynechat. Za tímto účelem by při implementaci metody měla být požadovaným parametrům přiřazena hodnota přímo v seznamu parametrů:

private void GetData(int Číslo, int Nepovinné = 5 )

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

Console.WriteLine("Volitelné: (0)", Volitelné);

V tomto případě můžete metodu volat následovně:

GetData(10, 20);

V prvním případě bude volitelný parametr roven 20, protože je výslovně specifikován, a ve druhém případě bude roven 5, protože není explicitně specifikována a kompilátor přebírá výchozí hodnotu.

Výchozí parametry lze nastavit pouze na pravé straně seznamu parametrů; například takový podpis metody nebude kompilátorem přijat:

private void GetData (int Volitelné = 5 , int Číslo)

Když jsou parametry předány metodě normálním způsobem (bez dalších klíčových slov ref a out), žádné změny parametrů v metodě neovlivní její hodnotu v hlavním programu. Řekněme, že máme následující metodu:

private void Calc (int Number)

Je vidět, že uvnitř metody se mění proměnná Number, která byla předána jako parametr. Zkusme zavolat metodu:

Console.WriteLine(n);

Na obrazovce se objeví číslo 1, to znamená, že i přes změnu proměnné v metodě Calc se hodnota proměnné v hlavním programu nezměnila. To je způsobeno skutečností, že když je volána metoda, a kopírovat předaná proměnná, je to právě tato proměnná, kterou metoda mění. Po ukončení metody se hodnota kopií ztratí. Tato metoda předání parametru se nazývá projít kolem hodnoty.

Aby metoda změnila proměnnou, která jí byla předána, musí být předána s klíčovým slovem ref – musí být jak v podpisu metody, tak při volání:

private void Calc (ref int Number)

Console.WriteLine(n);

V tomto případě se na obrazovce objeví číslo 10: změna hodnoty v metodě ovlivnila i hlavní program. Tato metoda se nazývá přenos míjení odkazem, tj. Již se nepřenáší kopie, ale odkaz na skutečnou proměnnou v paměti.

Pokud metoda používá proměnné odkazem pouze k vrácení hodnot a nezáleží jí na tom, co v nich původně bylo, nemůžete takové proměnné inicializovat, ale předat je pomocí klíčového slova out. Kompilátor chápe, že počáteční hodnota proměnné není důležitá a nestěžuje si na nedostatek inicializace:

private void Calc (out int Number)

int n; // My nic nepřidělujeme!

datový typ řetězce

Jazyk C# používá k ukládání řetězců typ řetězce. Chcete-li deklarovat (a zpravidla okamžitě inicializovat) řetězcovou proměnnou, můžete napsat následující kód:

řetězec a = "Text";

řetězec b = "řetězce";

Můžete provést operaci sčítání na řádcích - v tomto případě bude text jednoho řádku přidán k textu druhého:

řetězec c = a + " " + b; // Výsledek: Textový řetězec

Typ string je vlastně alias pro třídu String, která umožňuje provádět s řetězci řadu složitějších operací. Například metoda IndexOf může vyhledat podřetězec v řetězci a metoda Substring vrátí část řetězce zadané délky počínaje zadanou pozicí:

řetězec a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Výsledek: 14 (počítáno od 0)

řetězec b = a.Podřetězec(3, 5); // Výsledek: DEFGH

Pokud potřebujete do řetězce přidat speciální znaky, můžete to udělat pomocí escape sekvencí začínajících zpětným lomítkem:

Komponenta ListBox

Komponent Seznam je seznam, jehož prvky se vybírají pomocí klávesnice nebo myši. Seznam prvků je určen vlastností Položky. Předměty jsou prvkem, který má své vlastnosti a vlastní metody. Metody Přidat, RemoveAt A Vložit se používají k přidávání, odstraňování a vkládání prvků.

Objekt Položky ukládá objekty do seznamu. Objektem může být libovolná třída – data třídy jsou převedena pro zobrazení do řetězcové reprezentace metodou ToString. V našem případě budou řetězce fungovat jako objekty. Protože však objekt Items ukládá objekty přetypované na typ objektu, před jeho použitím je musíte přetypovat zpět na jejich původní typ, v našem případě řetězec:

string a = (string)listBox1.Items;

Chcete-li určit číslo vybraného prvku, použijte vlastnost SelectedIndex.

Když jsem začal programovat v C++ a intenzivně studoval knihy a články, vždy jsem narazil na stejnou radu: pokud potřebujeme předat funkci nějaký objekt, který by se ve funkci neměl měnit, pak by měl být vždy předán odkazem na konstantu(PPSK), s výjimkou případů, kdy potřebujeme předat buď primitivní typ nebo strukturu podobnou jim velikosti. Protože Za více než 10 let programování v C++ se s touto radou setkávám velmi často (a sám jsem ji dal nejednou), dávno se do mě „vstřebal“ – všechny argumenty automaticky předávám odkazem na konstantu . Jenže čas plyne a již uplynulo 7 let od doby, kdy jsme měli k dispozici C++11 s jeho move sémantikou, v souvislosti s níž slyším stále více hlasů zpochybňujících staré dobré dogma. Mnozí začínají tvrdit, že přechod odkazem na konstantu je minulostí a nyní je to nutné projít kolem hodnoty(PPZ). Co se za těmito rozhovory skrývá a také jaké závěry z toho všeho můžeme vyvodit, chci probrat v tomto článku.

Knižní moudrost

Abychom pochopili, jakého pravidla bychom se měli držet, doporučuji obrátit se na knihy. Knihy jsou výborným zdrojem informací, které nejsme povinni přijímat, ale které určitě stojí za to si je poslechnout. A začneme historií, původem. Nebudu zjišťovat, kdo byl prvním apologetem PPSC, jednoduše uvedu jako příklad knihu, která mě osobně v otázce používání PPSC nejvíce ovlivnila.

Mayers

Dobře, tady máme třídu, ve které jsou všechny parametry předávány odkazem, jsou s touto třídou nějaké problémy? Bohužel existuje a tento problém leží na povrchu. V naší třídě máme 2 funkční entity: první nabývá hodnoty ve fázi vytváření objektu a druhá umožňuje změnit dříve nastavenou hodnotu. Máme dvě entity, ale čtyři funkce. Nyní si představte, že nemůžeme mít 2 podobné entity, ale 3, 5, 6, co potom? Pak budeme čelit vážnému nafouknutí kódu. Proto, aby nedošlo k vytvoření množství funkcí, byl návrh zcela opustit odkazy v parametrech:

Šablona class Holder ( public: explicitní Holder(T value): m_Value(move(value)) ( ) void setValue(T value) (nem_Value = move(value); ) const T& value() const noexcept ( return m_Value; ) private: T m_Value; );

První výhodou, která vás okamžitě zaujme, je podstatně méně kódu. Je toho ještě méně než v úplně první verzi, kvůli odstranění const a & (i když přidali move ). Ale vždy nás učili, že míjet odkazem je produktivnější než míjet hodnotou! Tak to bylo před C++ 11 a jak to je stále, ale když se nyní podíváme na tento kód, uvidíme, že se zde nekopíruje více než v první verzi, za předpokladu, že T má konstruktor přesunu. Tito. Samotné PPSC bylo a bude rychlejší než PPZ, ale kód nějakým způsobem používá předanou referenci a často se tento argument kopíruje.

To však není celý příběh. Na rozdíl od první možnosti, kde máme pouze kopírování, zde přidáváme i pohyb. Ale stěhování je levná operace, ne? Na toto téma má Mayersova kniha, o které uvažujeme, také kapitolu („Položka 29“), která má název: „Předpokládejme, že operace přesunu nejsou k dispozici, nejsou levné a nepoužívají se.“ Hlavní myšlenka by měla být z názvu jasná, ale pokud chcete podrobnosti, určitě si to přečtěte - nebudu se tím zabývat.

Zde by bylo vhodné provést úplnou srovnávací analýzu první a poslední metody, ale nerad bych se od knihy odchýlil, proto analýzu odložíme na další oddíly a zde budeme nadále zvažovat Scottovy argumenty. Takže kromě skutečnosti, že třetí možnost je zjevně kratší než druhá, v čem Scott vidí výhodu PPZ oproti PPSC v moderním kódu?

Vidí to v tom, že v případě předání rvalue, tzn. někteří volají takto: Držitel držitel(string("já")); , varianta s PPSC nám poskytne kopírování a varianta s PPZ nám zajistí pohyb. Na druhou stranu, pokud je převod takto: Držitel držitel(nějakáLvalue); , pak PPZ definitivně prohrává díky tomu, že bude provádět jak kopírování, tak přesun, kdežto ve verzi s PPSC bude pouze jedno kopírování. Tito. ukazuje se, že PPZ, pokud uvažujeme čistě o efektivitě, je jakýmsi kompromisem mezi množstvím kódu a „plnou“ (přes && ) podporou sémantiky pohybu.

Proto Scott formuloval své rady tak pečlivě a tak pečlivě je propaguje. Dokonce se mi zdálo, že to nadnesl zdráhavě, jakoby pod tlakem: nemohl si pomoct, aby do knihy nezařadil diskuze na toto téma, protože... diskutovalo se o tom poměrně široce a Scott byl vždy sběratelem kolektivních zkušeností. Kromě toho uvádí velmi málo argumentů na obranu PPZ, ale uvádí mnoho těch, které tuto „techniku“ zpochybňují. Na jeho argumenty proti se podíváme v pozdějších částech, ale zde stručně zopakujeme argument, který Scott uvádí na obranu PPP (mentálně dodává „pokud objekt podporuje pohyb a je levný“): umožňuje vyhnout se kopírování při předávání výrazu rvalue jako argumentu funkce. Ale dost mučení Meyersovy knihy, přejděme k další knize.

Mimochodem, pokud někdo četl knihu a je překvapen, že zde nezahrnuji možnost s tím, co Mayers nazýval univerzální reference - nyní známé jako přeposílání - pak je to snadno vysvětlitelné. Uvažuji pouze o PPZ a PPSC, protože... Považuji za špatnou formu zavádění šablonových funkcí pro metody, které nejsou šablonami jen z důvodu podpory předávání odkazem obou typů (rvalue/lvalue). Nemluvě o tom, že kód vypadá jinak (už žádná neměnnost) a přináší s sebou další problémy.

Josattis a spol

Poslední knihou, na kterou se podíváme, je „C++ Templates“, která je také nejnovější ze všech knih zmíněných v tomto článku. Vyšlo na konci roku 2017 (a v knize je uveden rok 2018). Na rozdíl od jiných knih je tato celá věnována vzorům a ne radám (jako Mayers) nebo C++ obecně, jako Stroustrup. Proto jsou zde zvažovány klady/zápory z hlediska psaní šablon.

Tomuto tématu je věnována celá kapitola 7, která má výmluvný název „By value or by reference?“. V této kapitole autoři poměrně stručně, ale výstižně popisují všechny způsoby přenosu se všemi jejich klady a zápory. Rozbor efektivity zde prakticky není uveden a považuje se za samozřejmé, že PPSC bude rychlejší než PPZ. Ale s tím vším na konci kapitoly autoři doporučují použít výchozí PPP pro funkce šablon. Proč? Protože pomocí odkazu se parametry šablony zobrazí kompletně a bez odkazu se „rozpadnou“, což má příznivý vliv na zpracování polí a řetězcových literálů. Autoři se domnívají, že pokud se to pro některý typ PPP ukáže jako neúčinné, pak můžete vždy použít std::ref a std::cref . To je nějaká rada, abych byl upřímný, viděli jste mnoho lidí, kteří chtějí používat výše uvedené funkce?

Co radí ohledně PPSC? Doporučují používat PPSC, když je výkon kritický nebo existují jiné závažný důvody, proč nepoužívat PPP. Samozřejmě se zde bavíme pouze o standardním kódu, ale tato rada je v přímém rozporu se vším, co se programátoři učili po desetiletí. Toto není jen rada, abyste považovali PPP za alternativu – ne, toto je rada, abyste z PPSC udělali alternativu.

Tímto naše knižní turné končí, protože... Nevím o žádné jiné knize, kterou bychom měli o této problematice konzultovat. Přejděme k jinému mediálnímu prostoru.

Síťová moudrost

Protože Žijeme v době internetu, pak byste se neměli spoléhat pouze na knižní moudrost. Navíc mnozí autoři, kteří dříve psali knihy, nyní jednoduše píší blogy a knihy opustili. Jedním z těchto autorů je Herb Sutter, který v květnu 2013 publikoval na svém blogu „GotW #4 Solution: Class Mechanics“ článek, který se, i když není zcela věnován problému, o kterém se zabýváme, stále se ho dotýká.

Takže v původní verzi článku Sutter jednoduše zopakoval starou moudrost: „předat parametry odkazem na konstantu“, ale tuto verzi článku již neuvidíme, protože Článek obsahuje opačnou radu: „ Li parametr bude stále zkopírován a poté jej předán hodnotou." Opět notoricky známé „kdyby“. Proč Sutter změnil článek a jak jsem se o tom dozvěděl? Z komentářů. Přečtěte si komentáře k jeho článku, mimochodem, jsou zajímavější a užitečnější než samotný článek. Pravda, po napsání článku Sutter konečně změnil názor a už takové rady nedává. Změnu názoru lze nalézt v jeho projevu na CppCon v roce 2014: „Zpátky k základům! Základy moderního stylu C++“. Určitě se podívejte, přejdeme na další internetový odkaz.

A jako další tu máme hlavní programovací zdroj 21. století: StackOverflow. Nebo spíše odpověď, přičemž počet kladných reakcí v době psaní tohoto článku přesáhl 1700. Otázka zní: Co je to idiom kopírování a výměny? , a jak by název měl napovídat, ne úplně k tématu, na které se díváme. Ale ve své odpovědi na tuto otázku se autor dotýká i tématu, které nás zajímá. Doporučuje také používat PPZ „pokud se argument stejně zkopíruje“ (je čas zavést zkratku i pro toto, proboha). A obecně se tato rada jeví jako docela vhodná, v rámci jeho odpovědi a tam diskutovaného operátora=, ale autor si dovoluje takové rady poskytnout i v širším smyslu a nejen v tomto konkrétním případě. Navíc jde dále než všechny tipy, o kterých jsme dříve diskutovali, a vyzývá k tomu i v kódu C++03! Co vedlo autora k takovým závěrům?

Hlavní inspiraci zřejmě autor odpovědi čerpal z článku jiného autora knihy a vývojáře Boost.MPL na částečný úvazek – Davea Abrahamse. Článek se jmenuje „Chcete rychlost? Projděte kolem hodnoty.“ , a to bylo zveřejněno již v srpnu 2009, tzn. 2 roky před přijetím C++11 a zavedením sémantiky pohybu. Stejně jako v předchozích případech doporučuji, aby si čtenář článek přečetl sám, ale uvedu hlavní argumenty (ve skutečnosti existuje pouze jeden argument), které Dave uvádí ve prospěch PPZ: musíte použít PPZ , protože s ním dobře funguje optimalizace „skip copy“ ( copy elision ), která v PPSC chybí. Pokud si přečtete komentáře k článku, můžete vidět, že rady, které propaguje, nejsou univerzální, což potvrzuje i sám autor, když reaguje na výtky komentátorů. Článek však obsahuje výslovnou radu (pokyn) použít PPP, pokud bude argument přesto zkopírován. Mimochodem, pokud by to někoho zajímalo, můžete si přečíst článek “Chcete rychlost? Nepřekračujte (vždy) hodnotu.“ . Jak by měl nadpis naznačovat, tento článek je reakcí na Daveův článek, takže pokud jste četli první, určitě si přečtěte i tento!

Bohužel (pro někoho naštěstí) takové články a (ještě více) oblíbené odpovědi na populárních stránkách vedou k masivnímu používání pochybných technik (triviální příklad) prostě proto, že to vyžaduje méně psaní a staré dogma už není neotřesitelné - Vždy se můžete odkázat na „tu populární radu“, pokud vás přitlačí ke zdi. Nyní vám navrhuji, abyste se seznámili s tím, co nám různé zdroje nabízejí s doporučeními pro psaní kódu.

Protože Vzhledem k tomu, že různé standardy a doporučení jsou nyní zveřejňovány také online, rozhodl jsem se klasifikovat tuto sekci jako „síťová moudrost“. Rád bych zde tedy pohovořil o dvou zdrojích, jejichž účelem je zlepšit kód programátorů v C++ tím, že jim poskytneme tipy (návody), jak napsat právě tento kód.

První sada pravidel, která chci zvážit, byla poslední kapkou, která mě donutila tento článek přijmout. Tato sada je součástí nástroje Clang-tidy a neexistuje mimo něj. Stejně jako vše, co souvisí s clangem, je tato utilita velmi populární a již získala integraci s CLion a Resharper C++ (tak jsem na to narazil). Clang-tydy tedy obsahuje pravidlo modernizace-pass-by-value, které funguje na konstruktorech, které přijímají argumenty přes PPSC. Toto pravidlo navrhuje, abychom nahradili PPSC za PPZ. Navíc v době psaní článku popis tohoto pravidla obsahuje poznámku, že toto pravidlo sbohem funguje pouze pro konstruktéry, ale ti (kdo to jsou?) rádi přijmou pomoc od těch, kteří toto pravidlo rozšíří na další subjekty. V popisu je odkaz na Daveův článek - je jasné, odkud nohy pocházejí.

Na závěr, abych uzavřel tuto recenzi moudrosti a autoritativních názorů jiných lidí, navrhuji, abyste se podívali na oficiální pokyny pro psaní kódu C++: C++ Core Guidelines, jejichž hlavními editory jsou Herb Sutter a Bjarne Stroustrup (není špatné, že?). Tato doporučení tedy obsahují následující pravidlo: „Pro parametry „in“ předávejte levně zkopírované typy hodnotou a ostatní odkazem na const“, což zcela opakuje starou moudrost: PPSK všude a PPP pro malé objekty. Tento tip nastiňuje několik alternativ, které je třeba zvážit. v případě, že předávání argumentů potřebuje optimalizaci. Ale PPZ není zařazen do seznamu alternativ!

Protože nemám žádné další zdroje hodné pozornosti, navrhuji přejít k přímé analýze obou způsobů přenosu.

Analýza

Celý předchozí text je napsán pro mě poněkud nezvyklým způsobem: prezentuji názory jiných lidí a dokonce se snažím nevyjadřovat svůj (vím, že to dopadá špatně). Z velké části kvůli tomu, že názory ostatních, a mým cílem bylo udělat jejich stručný přehled, jsem odložil podrobné zvážení určitých argumentů, které jsem našel u jiných autorů. V této části nebudu odkazovat na úřady a vyjadřovat názory, podíváme se zde na některé objektivní výhody a nevýhody PPSC a PPZ, které budou okořeněny mým subjektivním vnímáním. Samozřejmě se bude opakovat něco z toho, co bylo probráno dříve, ale bohužel, toto je struktura tohoto článku.

Má PPP výhodu?

Než tedy zvážím argumenty pro a proti, navrhuji se podívat na to, co a v jakých případech nám dává výhoda, kterou nám míjení hodnoty přináší. Řekněme, že máme třídu jako je tato:

Třída CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByAuterNot_ValuerValuer aluerAndNotMover = byVal uerAndNotMover; ) void setRvaluer (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Přestože nás pro účely tohoto článku zajímají pouze první dvě funkce, zahrnul jsem čtyři možnosti, jak je použít jako kontrast.

Třída Accounter je jednoduchá třída, která počítá, kolikrát byla zkopírována/přesunuta. A ve třídě CopyMover jsme implementovali funkce, které nám umožňují zvážit následující možnosti:

    pohybující se prošel argument.

    Přechod podle hodnoty, následovaný kopírování prošel argument.

Nyní, když předáme lvalue každé z těchto funkcí, například takto:

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

pak dostaneme následující výsledky:

Jednoznačným vítězem je PPSC, protože... dává pouze jednu kopii, zatímco PPZ dává jednu kopii a jeden tah.

Nyní zkusme předat rvalue:

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

Získáme následující:

Tady není jasný vítěz, protože... PPZ i PPSK mají po jedné operaci, ale vzhledem k tomu, že PPZ používá pohyb a PPSK kopírování, můžeme dát vítězství PPZ.

Tím ale naše experimenty nekončí; pojďme přidat následující funkce pro simulaci nepřímého volání (s následným předáním argumentu):

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

Použijeme je úplně stejně jako bez nich, takže kód nebudu opakovat (v případě potřeby se podívejte do úložiště). Takže pro lvalue by výsledky vypadaly takto:

Všimněte si, že PPSC zvětšuje mezeru s PPZ a zůstává u jediné kopie, zatímco PPZ již má až 3 operace (jeden pohyb navíc)!

Nyní předáme rvalue a získáme následující výsledky:

Nyní má PPZ 2 pohyby a PPSC má stále jednu kopii. Je nyní možné nominovat PPZ jako vítěze? Ne, protože pokud by jeden tah neměl být horší než jedna kopie, nemůžeme totéž říci o 2 tazích. V tomto příkladu tedy nebude vítěz.

Mohou mi namítnout: „Autore, máš zaujatý názor a taháš za to, co je pro tebe výhodné. I 2 tahy budou levnější než kopírování!“ S tímto tvrzením nemohu souhlasit Celkově vzato, protože O kolik rychlejší je přesun než kopírování závisí na konkrétní třídě, ale na „levné“ přesunutí se podíváme v samostatné části.

Zde jsme se dotkli zajímavé věci: přidali jsme jedno nepřímé volání a PPP přidalo přesně jednu operaci ve „váhu“. Myslím, že nepotřebujete mít diplom z MSTU, abyste pochopili, že čím více nepřímých hovorů máme, tím více operací bude prováděno při použití PPZ, zatímco u PPSC zůstane číslo nezměněno.

Vše, co bylo uvedeno výše, pravděpodobně nebude pro nikoho zjevením, možná jsme ani neprováděli experimenty – všechna tato čísla by měla být většině programátorů v C++ na první pohled zřejmá. Pravda, jeden bod si ještě zaslouží upřesnění: proč v případě rvalue nemá PZ kopii (nebo jiný tah), ale pouze jeden tah.

No, podívali jsme se na rozdíl v přenosu mezi PPZ a PPSC tak, že jsme z první ruky sledovali počet kopií a tahů. I když je zřejmé, že výhoda PPZ oproti PPSC i v takto jednoduchých příkladech je, mírně řečeno Ne Je zřejmé, že stále dělám, trochu domýšlivě, následující závěr: pokud budeme stále kopírovat argument funkce, pak má smysl zvážit předání argumentu funkci hodnotou. Proč jsem vyvodil tento závěr? Pro hladký přechod k další části.

Pokud zkopírujeme...

Dostáváme se tedy k pověstnému „kdyby“. Většina argumentů, s nimiž jsme se setkali, nevyžadovala univerzální implementaci PPP namísto PPSC; požadovali to pouze „pokud se argument stejně zkopíruje“. Je čas zjistit, co je na tomto argumentu špatného.

Chci začít malým popisem toho, jak píšu kód. Poslední dobou se můj proces kódování stále více podobá TDD, tzn. zápis jakékoli metody třídy začíná napsáním testu, ve kterém se tato metoda objeví. V souladu s tím, když začínám psát test a vytvářím metodu po napsání testu, stále nevím, zda argument zkopíruji. Samozřejmě ne všechny funkce jsou vytvořeny tímto způsobem, často i v procesu psaní testu přesně víte, o jakou implementaci půjde. Ale ne vždy se to stane!

Někdo by mi mohl namítnout, že nezáleží na tom, jak byla metoda původně napsána, můžeme změnit, jak předáme argument, když se metoda zformovala a je nám zcela jasné, co se tam děje (tj. zda máme kopírování, popř. ne). Částečně s tím souhlasím - opravdu, můžete to udělat tímto způsobem, ale to nás zapojí do jakési podivné hry, kde musíme změnit rozhraní jen proto, že se změnila implementace. Což nás přivádí k dalšímu dilematu.

Ukazuje se, že rozhraní upravujeme (nebo dokonce plánujeme) podle toho, jak bude implementováno. Nepovažuji se za odborníka na OOP a další teoretické výpočty softwarové architektury, ale takové akce jasně odporují základním pravidlům, kdy by implementace neměla ovlivnit rozhraní. Samozřejmě, že určité detaily implementace (ať už se jedná o vlastnosti jazyka nebo cílové platformy) stále unikají přes rozhraní tak či onak, ale měli byste se snažit počet takových věcí snižovat, nikoli zvyšovat.

Bůh mu žehnej, pojďme touto cestou a stále měňte rozhraní v závislosti na tom, co děláme v implementaci ve smyslu kopírování argumentu. Řekněme, že jsme napsali tuto metodu:

Void setName (jméno jméno) ( m_Name = move(name); )

a provedli změny v úložišti. Postupem času náš softwarový produkt získal novou funkcionalitu, byly integrovány nové frameworky a vyvstal úkol informovat okolní svět o změnách v naší třídě. Tito. Do naší metody přidáme nějaký oznamovací mechanismus, ať je to něco podobného signálům Qt:

Void setName(jméno jméno) ( m_Name = move(name); emit nameChanged(m_Name); )

Je s tímto kódem problém? Jíst. Pro každé volání setName vysíláme signál, takže signál bude odeslán, i když význam m_Name se nezměnilo. Kromě problémů s výkonem může tato situace vést k nekonečné smyčce kvůli tomu, že kód, který obdrží výše uvedené oznámení, nějak začne volat setName . Aby se předešlo všem těmto problémům, tyto metody nejčastěji vypadají takto:

Void setName(jméno jméno) ( if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); )

Zbavili jsme se výše popsaných problémů, ale nyní selhalo naše pravidlo „pokud přesto zkopírujeme...“ – již není bezpodmínečné kopírování argumentu, nyní jej zkopírujeme pouze v případě, že se změní! Co bychom tedy nyní měli dělat? Změnit rozhraní? Dobře, změňme rozhraní třídy kvůli této opravě. Co když naše třída zdědila tuto metodu z nějakého abstraktního rozhraní? Pojďme to změnit i tam! Je tam hodně změn, protože se změnila implementace?

Opět mi mohou namítat, říkají, autore, proč se snažíte šetřit na zápalkách, když tam tato podmínka vyjde? Ano, většina hovorů bude falešná! Je v tom nějaká důvěra? Kde? A pokud jsem se rozhodl ušetřit na zápasech, nebyl právě fakt, že jsme použili PPZ, důsledkem právě takové úspory? Jen pokračuji v „linii strany“, která obhajuje efektivitu.

Konstruktéři

Pojďme si krátce projít konstruktory, zejména proto, že pro ně v clang-tidy existuje speciální pravidlo, které zatím nefunguje pro jiné metody/funkce. Řekněme, že máme třídu jako je tato:

Třída JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Je zřejmé, že parametr je zkopírován a clang-tidy nám řekne, že by bylo dobré přepsat konstruktor na toto:

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

A upřímně řečeno, je pro mě těžké se zde hádat - koneckonců opravdu vždy kopírujeme. A nejčastěji, když něco předáváme konstruktorem, tak to kopírujeme. Ale častěji neznamená vždy. Zde je další příklad:

Třída 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; );

Zde nekopírujeme vždy, ale pouze tehdy, když jsou data uvedena správně. Samozřejmě v drtivé většině případů tomu tak bude. Ale ne vždy.

Můžete uvést další příklad, ale tentokrát bez kódu. Představte si, že máte třídu, která přijímá velký objekt. Třída existuje již dlouhou dobu a nyní je čas aktualizovat její implementaci. Uvědomujeme si, že nepotřebujeme více než polovinu velkého zařízení (které se v průběhu let rozrostlo), a možná ještě méně. Můžeme s tím něco udělat tím, že budeme předávat hodnotu? Ne, nebudeme moci nic dělat, protože se stále vytvoří kopie. Ale pokud bychom používali PPSC, jednoduše bychom změnili to, co děláme uvnitř návrhář. A to je klíčový bod: pomocí PPSC kontrolujeme, co a kdy se stane při implementaci naší funkce (konstruktoru), ale pokud použijeme PPZ, pak ztratíme jakoukoli kontrolu nad kopírováním.

Co si z této sekce odnést? Skutečnost, že argument „když budeme kopírovat...“ je velmi kontroverzní, protože Ne vždy víme, co budeme kopírovat, a i když to víme, velmi často si nejsme jisti, že to bude pokračovat i v budoucnu.

Stěhování je levné

Od chvíle, kdy se objevila sémantika pohybu, začala mít vážný vliv na způsob psaní moderního kódu C++ a postupem času tento vliv jen zesílil: není divu, protože pohyb je takový levný ve srovnání s kopírováním. Ale je to tak? Je pravda, že pohyb je Vždy levná operace? To se pokusíme zjistit v této části.

Binární velký objekt

Začněme triviálním příkladem, řekněme, že máme následující třídu:

Struct Blob ( std::array data; );

Obyčejný kapka(BDO, anglicky BLOB), které lze použít v různých situacích. Podívejme se na to, co nás bude stát projít podle reference a podle hodnoty. Naše BDO bude použito takto:

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

A budeme tyto funkce nazývat takto:

Const Blob blob(); úložný prostor; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Kód pro další příklady bude totožný s tímto, pouze s jinými názvy a typy, takže jej u zbývajících příkladů neuvedu - vše je v úložišti.

Než přejdeme k měření, zkusme předpovědět výsledek. Máme tedy 4 KB std::array, které chceme uložit do objektu třídy Storage. Jak jsme zjistili dříve, pro PPSC budeme mít jednu kopii, zatímco pro PPZ budeme mít jednu kopii a jeden tah. Vzhledem k tomu, že pole nelze přesunout, budou 2 kopie pro PPZ oproti jedné pro PPSC. Tito. můžeme očekávat dvojnásobnou převahu ve výkonu pro PPSC.

Nyní se podívejme na výsledky testu:

Tento a všechny následující testy byly spuštěny na stejném počítači pomocí MSVS 2017 (15.7.2) a příznaku /O2.

Praxe se shodovala s předpokladem - předávání hodnoty je 2x dražší, protože pro pole je přesun zcela ekvivalentní kopírování.

Čára

Podívejme se na další příklad, běžný std::string . Co můžeme očekávat? Víme (o tom jsem hovořil v článku), že moderní implementace rozlišují dva typy řetězců: krátký (kolem 16 znaků) a dlouhý (ty, které jsou delší než krátké). Pro krátké se používá vnitřní buffer, což je běžné C-pole char , ale dlouhé už budou umístěny na hromadu. Krátké řádky nás nezajímají, protože... výsledek tam bude stejný jako u BDO, takže se zaměřme na dlouhé linky.

Takže s dlouhým řetězcem je zřejmé, že přesunutí by mělo být docela levné (stačí pohnout ukazatelem), takže počítejte s tím, že přesunutí řetězce by nemělo vůbec ovlivnit výsledky a PPZ by měl dát výsledek není horší než PPSC. Pojďme si to ověřit v praxi a získat následující výsledky:

Přejdeme k vysvětlení tohoto „fenoménu“. Co se tedy stane, když zkopírujeme existující řetězec do již existujícího řetězce? Podívejme se na triviální příklad:

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

Máme dva řetězce o 64 znacích, takže vnitřní vyrovnávací paměť je při jejich vytváření nedostatečná, což má za následek alokaci obou řetězců na haldě. Nyní zkopírujeme první do druhého. Protože naše velikosti řádků jsou stejné, je zřejmé, že v sekundě je vyhrazeno dostatek místa pro všechna data z první, takže druhá = první; bude banální memcpy, nic víc. Ale když se podíváme na mírně upravený příklad:

String first(64, "C"); řetězec druhý = první;

pak již nebude volání operátoru=, ale bude zavolán konstruktor kopírování. Protože Protože máme co do činění s konstruktorem, není v něm žádná existující paměť. Musí být nejprve vybrán a teprve poté zkopírován. Tito. toto je alokace paměti a pak memcpy. Jak vy i já víme, alokace paměti na globální haldě je obvykle nákladná operace, takže kopírování z druhého příkladu bude dražší než kopírování z prvního. Dražší na přidělení paměti haldy.

Co to má společného s naším tématem? Nejpřímější, protože první příklad ukazuje přesně to, co se stane s PPSC, a druhý ukazuje, co se stane s PPZ: pro PPZ se vždy vytvoří nový řádek, zatímco pro PPSC se znovu použije stávající. Rozdíl v době provedení jste již viděli, takže zde není co dodat.

Zde se opět setkáváme s tím, že při využívání PPP pracujeme mimo kontext, a proto nemůžeme využít všech výhod, které může poskytnout. A pokud jsme dříve uvažovali z hlediska teoretických budoucích změn, zde pozorujeme velmi konkrétní selhání v produktivitě.

Samozřejmě, někdo by mi mohl namítnout, že řetězec stojí stranou a většina typů takto nefunguje. Na to mohu odpovědět následovně: vše popsané dříve bude platit pro jakýkoli kontejner, který okamžitě alokuje paměť v hromadě pro balíček prvků. Také, kdo ví, jaké další kontextově citlivé optimalizace se používají v jiných typech?

Co byste si z této sekce měli odnést? To, že i když je stěhování opravdu levné, neznamená, že nahrazení kopírování kopírováním+přesouváním vždy přinese výkonově srovnatelný výsledek.

Komplexní typ

Nakonec se podívejme na typ, který se bude skládat z více objektů. Nechť je to třída Osoba, která se skládá z dat vlastní osobě. Obvykle se jedná o vaše křestní jméno, příjmení, PSČ atd. To vše můžete reprezentovat jako řetězce a předpokládat, že řetězce, které vložíte do polí třídy Person, budou pravděpodobně krátké. I když věřím, že v reálném životě bude měření krátkých strun nejužitečnější, přesto se podíváme na struny různých velikostí, abychom si udělali ucelenější obrázek.

Použiji také Osoba s 10 poli, ale k tomu nebudu vytvářet 10 polí přímo v těle třídy. Implementace Person ve svých hloubkách skrývá kontejner – díky tomu je pohodlnější měnit parametry testu, prakticky bez odchylky od toho, jak by to fungovalo, kdyby Person byla skutečná třída. Implementace je však k dispozici a vždy můžete zkontrolovat kód a říct mi, jestli jsem něco udělal špatně.

Takže pojďme na to: Osoba s 10 poli typu string , které přeneseme pomocí PPSC a PPZ do Storage :

Jak vidíte, máme obrovský rozdíl ve výkonu, což by po předchozích dílech nemělo být pro čtenáře překvapením. Také se domnívám, že třída Person je natolik „skutečná“, že takové výsledky nebudou považovány za abstraktní.

Mimochodem, když jsem připravoval tento článek, připravil jsem další příklad: třídu, která používá několik objektů std::function. Podle mé představy to mělo také vykazovat výhodu ve výkonu PPSC oproti PPZ, ale dopadlo to přesně naopak! Ale tento příklad zde neuvádím proto, že by se mi výsledky nelíbily, ale protože jsem neměl čas přijít na to, proč byly takové výsledky získány. Přesto je v úložišti kód (Tiskárny), testy - taky, kdyby na to někdo chtěl přijít, budu rád, když se dozvím o výsledcích výzkumu. K tomuto příkladu se plánuji vrátit později, a pokud tyto výsledky přede mnou nikdo nezveřejní, pak se jim budu věnovat v samostatném článku.

Výsledek

Podívali jsme se tedy na různé výhody a nevýhody předávání podle hodnoty a předávání odkazem na konstantu. Podívali jsme se na některé příklady a podívali jsme se na výkon obou metod v těchto příkladech. Tento článek samozřejmě nemůže a není vyčerpávající, ale podle mého názoru obsahuje dostatek informací pro nezávislé a informované rozhodnutí o tom, kterou metodu je nejlepší použít. Někdo může namítnout: "Proč používat jednu metodu, začněme od úkolu!" I když s touto tezí obecně souhlasím, v této situaci s ní nesouhlasím. Věřím, že v jazyce může být pouze jeden způsob předávání argumentů, který se standardně používá.

Co znamená výchozí? To znamená, že když píšu funkci, nepřemýšlím o tom, jak bych měl argument předat, prostě použiji "výchozí". Jazyk C++ je poměrně složitý jazyk, kterému se mnoho lidí vyhýbá. A podle mého názoru je složitost způsobena ani ne tak složitostí jazykových konstruktů, které v jazyce existují (typický programátor se s nimi nemusí nikdy setkat), ale tím, že vás jazyk nutí hodně přemýšlet: osvobodil jsem up memory, je drahé používat tuto funkci zde? a tak dále.

Mnoho programátorů (C, C++ a další) je nedůvěřivé a bojí se C++, které se začalo objevovat po roce 2011. Slyšel jsem spoustu kritiky, že jazyk se stává složitějším, že v něm nyní mohou psát pouze „guruové“ atd. Osobně se domnívám, že tomu tak není - naopak komise věnuje hodně času tomu, aby byl jazyk přátelštější k začátečníkům a aby programátoři museli méně přemýšlet o vlastnostech jazyka. Koneckonců, pokud nemusíme bojovat s jazykem, pak máme čas o úkolu přemýšlet. Tato zjednodušení zahrnují inteligentní ukazatele, funkce lambda a mnoho dalšího, co se v jazyce objevilo. Zároveň nepopírám, že teď je potřeba více studovat, ale co je na studiu špatného? Nebo nedochází k žádným změnám v jiných populárních jazycích, které je třeba se naučit?

Dále nepochybuji, že se najdou snobové, kteří dokážou odpovědět: „Nechceš přemýšlet? Pak jděte psát v PHP." Takovým lidem se ani odpovídat nechci. Uvedu jen příklad z herní reality: v prvním díle Starcraftu, když se v budově vytvoří nový dělník, aby mohl začít těžit nerosty (nebo plyn), musel tam být ručně poslán. Navíc každé balení minerálů mělo limit, po jehož dosažení byl nárůst dělníků zbytečný a dokonce se mohly navzájem rušit a zhoršovat produkci. To bylo změněno ve Starcraft 2: pracovníci automaticky začnou těžit nerosty (nebo plyn) a také to ukazuje, kolik pracovníků aktuálně těží a kolik je limit tohoto ložiska. To značně zjednodušilo interakci hráče se základnou, což mu umožnilo soustředit se na důležitější aspekty hry: budování základny, hromadění jednotek a ničení nepřítele. Zdálo by se, že je to jen skvělá inovace, ale co začalo na internetu! Lidé (kdo jsou oni?) začali křičet, že hra byla „podělaná“ a „zabili Starcraft“. Je zřejmé, že takové zprávy mohly pocházet pouze od „strážců tajných znalostí“ a „adeptů vysoké APM“, kteří byli rádi v nějakém „elitní“ klubu.

Takže, když se vrátím k našemu tématu, čím méně potřebuji přemýšlet o tom, jak napsat kód, tím více času mám na přemýšlení o řešení okamžitého problému. Přemýšlení o tom, kterou metodu bych měl použít - PPSC nebo PPZ - mě ani o kousíček nepřiblíží k vyřešení problému, takže o takových věcech prostě odmítám přemýšlet a volím jednu možnost: přejít odkazem na konstantu. Proč? Protože nevidím žádné výhody pro PPP v obecných případech a zvláštní případy je třeba posuzovat samostatně.

Je to zvláštní případ, jen když jsem si všiml, že v nějaké metodě se PPSC ukáže jako úzké hrdlo a změnou převodovky na PPZ získáme důležité zvýšení výkonu, neváhám použít PPZ. Ale standardně budu používat PPSC jak v běžných funkcích, tak v konstruktorech. A pokud to bude možné, budu tuto konkrétní metodu propagovat všude, kde to bude možné. Proč? Protože si myslím, že praxe propagace PPP je zlá, protože lví podíl programátorů není příliš znalý (buď v principu, nebo se prostě ještě nedostal do švihu) a prostě se řídí radami. Navíc, pokud existuje několik protichůdných rad, vyberou si tu, která je jednodušší, a to vede k pesimismu v kódu jednoduše proto, že někdo někde něco slyšel. Ach ano, tento někdo může také poskytnout odkaz na Abrahamsův článek, aby dokázal, že má pravdu. A pak sedíte, čtete kód a přemýšlíte: je fakt, že parametr je zde předán hodnotou, protože programátor, který to napsal, pocházel z Javy, jen si přečetl spoustu „chytrých“ článků, nebo je opravdu potřeba technické specifikace?

Čtení PPSC je mnohem snazší: člověk jasně zná „dobrou formu“ C++ a jedeme dál – pohled nezdržuje. Praxe používání PPSC byla učena programátory C++ léta, jaký je důvod, proč ji opustit? To mě vede k dalšímu závěru: pokud rozhraní metody používá PPP, pak by měl být také komentář, proč tomu tak je. V ostatních případech je nutné použít PPSC. Samozřejmě existují typy výjimek, ale nezmiňuji je zde jednoduše proto, že jsou implikované: string_view , initializer_list , různé iterátory atd. Jde ale o výjimky, jejichž seznam se může rozšiřovat podle toho, jaké typy jsou v projektu použity. Ale podstata zůstává stejná od C++98: standardně vždy používáme PPCS.

Pro std::string s největší pravděpodobností nebude žádný rozdíl na malých řetězcích, o tom si povíme později.

Předem se omlouvám za okázalou anotaci o „umístění bodů“, ale musíme vás do článku nějak nalákat)) Z mé strany se pokusím zajistit, aby abstrakt stále splňoval vaše očekávání.

Stručně, o čem mluvíme

Každý to už ví, ale na začátku vám připomenu, jak lze parametry metody předávat v 1C. Mohou být předány „odkazem“ nebo „hodnotou“. V prvním případě předáme metodě stejnou hodnotu jako v bodě volání a ve druhém její kopii.

Ve výchozím nastavení jsou v 1C argumenty předávány odkazem a změny parametru uvnitř metody budou viditelné zvenčí metody. Zde další pochopení otázky závisí na tom, co přesně chápete pod slovem „změna parametru“. Takže to znamená přeřazení a nic víc. Kromě toho může být přiřazení implicitní, například voláním metody platformy, která vrací něco ve výstupním parametru.

Pokud ale nechceme, aby byl náš parametr předán odkazem, pak můžeme před parametrem zadat klíčové slovo Význam

Procedure ByValue(Parametr hodnoty) Parametr = 2; Parametr EndProcedure = 1; ByValue(Parametr); Zpráva (Parametr); // vytiskne 1

Vše funguje tak, jak bylo slíbeno - změna (nebo spíše „nahrazení“) hodnoty parametru nemění hodnotu mimo metodu.

No, v čem je vtip?

Zajímavé momenty začínají, když jako parametry začneme předávat nikoli primitivní typy (řetězce, čísla, data atd.), ale objekty. Zde vstupují do hry pojmy jako „mělké“ a „hluboké“ kopie objektu a také ukazatele (ne v C++, ale jako abstraktní úchyty).

Při předávání objektu (například tabulky hodnot) odkazem předáváme samotnou hodnotu ukazatele (určitý úchyt), který „udrží“ objekt v paměti platformy. Po předání hodnotou platforma vytvoří kopii tohoto ukazatele.

Jinými slovy, pokud při předávání objektu odkazem v metodě přiřadíme parametru hodnotu „Array“, pak v bodě volání obdržíme pole. Nové přiřazení hodnoty předané odkazem je viditelné z místa volání.

Procedure ProcessValue(Parameter) Parametr = Nové pole; EndProcedure Table = New ValueTable; ProcessValue(Tabulka); Zpráva(Typ hodnoty(Tabulka)); // vypíše pole

Pokud předáme objekt hodnotou, pak se v bodě volání naše tabulka hodnot neztratí.

Obsah a stav objektu

Při předávání hodnotou se nekopíruje celý objekt, ale pouze jeho ukazatel. Instance objektu zůstává stejná. Nezáleží na tom, jak předáte objekt, podle odkazu nebo podle hodnoty - vymazáním tabulky hodnot vymažete samotnou tabulku. Toto čištění bude vidět všude, protože... byl pouze jeden objekt a nezáleželo na tom, jak přesně byl předán metodě.

Procedure ProcessValue(Parameter) Parametr.Clear(); EndProcedure Table = New ValueTable; Table.Add(); ProcessValue(Tabulka); Report(Table.Quantity()); // vypíše 0

Při předávání objektů metodám platforma pracuje s ukazateli (podmíněné, nikoli přímé analogy z C++). Pokud je objekt předán odkazem, pak může být paměťová buňka virtuálního stroje 1C, ve kterém objekt leží, přepsána jiným objektem. Pokud je objekt předán hodnotou, pak se ukazatel zkopíruje a přepsání objektu nevede k přepsání místa v paměti původním objektem.

Zároveň jakákoliv změna Stát objekt (čištění, přidání vlastností atd.) mění samotný objekt a nemá vůbec nic společného s tím, jak a kam byl objekt přenesen. Stav instance objektu se změnil; může na ni být spousta „referencí“ a „hodnot“, ale instance je vždy stejná. Předáním objektu metodě nevytváříme kopii celého objektu.

A to je vždy pravda, kromě...

Interakce klient-server

Platforma implementuje volání serveru velmi transparentně. Jednoduše zavoláme metodu a platforma pod kapotou serializuje (promění se v řetězec) všechny parametry metody, předá je serveru a poté vrátí výstupní parametry zpět klientovi, kde jsou deserializovány a fungují jako kdyby nikdy nebyli na žádném serveru.

Jak víte, ne všechny objekty platformy lze serializovat. Zde narůstá omezení: ne všechny objekty mohou být předány metodě serveru z klienta. Pokud předáte objekt, který nelze serializovat, platforma začne používat sprostá slova.

  • Výslovné prohlášení o záměrech programátora. Když se podíváte na signaturu metody, můžete jasně říci, které parametry jsou vstupní a které výstupní. Tento kód se snadněji čte a udržuje
  • Aby byla změna v parametru „podle reference“ na serveru viditelná na call pointu na klientovi, str Platforma sama nutně vrátí parametry předané serveru prostřednictvím odkazu na klienta, aby bylo zajištěno chování popsané na začátku článku. Pokud parametr není nutné vracet, dojde k přetečení provozu. Pro optimalizaci výměny dat by měly být parametry, jejichž hodnoty na výstupu nepotřebujeme, označeny slovem Value.

Druhý bod je zde pozoruhodný. Pro optimalizaci provozu platforma nevrátí hodnotu parametru klientovi, pokud je parametr označen slovem Value. To vše je skvělé, ale vede to k zajímavému efektu.

Jak jsem již řekl, při přenosu objektu na server dochází k serializaci, tzn. je provedena "hluboká" kopie objektu. A pokud existuje slovo Význam objekt nebude putovat ze serveru zpět ke klientovi. Přidáme tyto dvě skutečnosti a získáme následující:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Parametr hodnoty) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &Procedura OnClient CheckValue() List1= New ListValues; List1.Add("ahoj"); Seznam2 = Seznam1.Kopírovat(); Seznam3 = Seznam1.Kopírovat(); // objekt je zcela zkopírován, // přenesen na server a poté vrácen. // vymazání seznamu je viditelné u volacího bodu ByRef(List1); // objekt je kompletně zkopírován, // přenesen na server. Nevrací se to. // Vymazání seznamu NENÍ VIDITELNÉ v okamžiku volání ByValue(List2); // zkopíruje se pouze ukazatel objektu // vymazání seznamu je viditelné v místě volání ByValueClient(List3); Zpráva(Seznam1.Množství()); Zpráva(Seznam2.Množství()); Report(List3.Quantity()); Konec procedury

souhrn

Ve zkratce se to dá shrnout takto:

  • Předání odkazem umožňuje „přepsat“ objekt zcela jiným objektem
  • Předání hodnoty vám neumožňuje „přepsat“ objekt, ale změny ve vnitřním stavu objektu budou viditelné, protože pracujeme se stejnou instancí objektu
  • Při volání serveru se pracuje s RŮZNÝMI instancemi objektu, protože Byla provedena hluboká kopie. Klíčové slovo Význam zabrání zkopírování instance serveru zpět do instance klienta a změna vnitřního stavu objektu na serveru nepovede k podobné změně na klientovi.

Doufám, že tento jednoduchý seznam pravidel vám usnadní řešení sporů s kolegy ohledně předávání parametrů „podle hodnoty“ a „podle reference“