Prođite po vrijednosti. Prenošenje parametara po referenci i po vrijednosti. Default Settings

Dakle, neka je Faktorijal(n) funkcija za izračunavanje faktorijala broja n. Zatim, s obzirom da "znamo" da je faktorijel 1 1, možemo konstruirati sljedeći lanac:

Faktorski(4)=Faktorijalni(3)*4

Faktorski(3)=Faktorijalni(2)*3

Faktorski(2)=Faktorijalni(1)*2

Ali, da nemamo terminalni uslov da kada je n=1 Faktorska funkcija treba da vrati 1, onda takav teoretski lanac nikada ne bi završio, a ovo bi mogla biti greška prelivanja steka poziva - prekoračenje steka poziva. Da bismo razumjeli šta je stog poziva i kako se može preliti, pogledajmo rekurzivnu implementaciju naše funkcije:

Faktorijal funkcije (n: Integer): LongInt;

Ako je n=1 Onda

Faktorski:=Faktorijalni(n-1)*n;

End;

Kao što vidimo, da bi lanac radio ispravno, prije svakog sljedećeg poziva funkcije samoj sebi potrebno je negdje spremiti sve lokalne varijable, tako da kada se lanac obrne, rezultat bude ispravan (izračunata vrijednost faktorijala od n-1 se množi sa n). U našem slučaju, svaki put kada se faktorijalna funkcija pozove iz same sebe, sve vrijednosti varijable n moraju biti sačuvane. Područje u kojem se pohranjuju lokalne varijable funkcije prilikom rekurzivnog pozivanja naziva se stek poziva. Naravno, ovaj stog nije beskonačan i može biti iscrpljen ako se rekurzivni pozivi konstruišu pogrešno. Konačnost iteracija našeg primjera je zajamčena činjenicom da kada je n=1 poziv funkcije prestaje.

Prenošenje parametara po vrijednosti i referenci

Do sada nismo mogli promijeniti vrijednost u potprogramu stvarni parametar(tj. parametar koji je specificiran prilikom pozivanja potprograma), a u nekim zadacima aplikacije to bi bilo zgodno. Prisjetimo se procedure Val, koja mijenja vrijednost dva svoja stvarna parametra odjednom: prvi je parametar u koji će biti upisana konvertovana vrijednost string varijable, a drugi je parametar Code, gdje je broj pogrešnih karakter se postavlja u slučaju greške tokom konverzije tipa. One. još uvijek postoji mehanizam pomoću kojeg potprogram može promijeniti stvarne parametre. To je moguće zahvaljujući različitim načinima prenošenja parametara. Pogledajmo bliže ove metode.

Programiranje u Pascalu

Prenošenje parametara po vrijednosti

U suštini, ovako smo proslijedili sve parametre našim rutinama. Mehanizam je sljedeći: kada se specificira stvarni parametar, njegova vrijednost se kopira u memorijsku oblast u kojoj se nalazi potprogram, a zatim, nakon što funkcija ili procedura završi svoj rad, ovo područje se briše. Grubo govoreći, dok je potprogram pokrenut, postoje dvije kopije njegovih parametara: jedna u opsegu pozivajućeg programa, a druga u opsegu funkcije.

Kod ovog načina prosljeđivanja parametara potrebno je više vremena za pozivanje potprograma, jer je pored samog poziva potrebno kopirati sve vrijednosti svih stvarnih parametara. Ako se velika količina podataka prosljeđuje potprogramu (na primjer, niz sa velikim brojem elemenata), vrijeme potrebno za kopiranje podataka u lokalno područje može biti značajno i to se mora uzeti u obzir pri razvoju programa i pronalaženje uskih grla u njihovom izvođenju.

Sa ovom metodom prijenosa, stvarni parametri se ne mogu mijenjati od strane potprograma, jer će promjene utjecati samo na izolirano lokalno područje, koje će biti oslobođeno nakon što se funkcija ili procedura završi.

Prenošenje parametara prema referenci

Ovom metodom se vrijednosti stvarnih parametara ne kopiraju u potprogram, već se prenose adrese u memoriji (linkovi na varijable) gdje se nalaze. U ovom slučaju, potprogram već mijenja vrijednosti koje nisu u lokalnom opsegu, tako da će sve promjene biti vidljive pozivnom programu.

Da bi se naznačilo da se argument mora proslijediti referencom, ključna riječ var se dodaje prije njegove deklaracije:

Procedura getTwoRandom(var n1, n2:Integer; opseg: Integer);

n1:=random(opseg);

n2:=random(opseg); kraj ;

var rand1, rand2: Integer;

Počni getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Kraj.

U ovom primjeru, reference na dvije varijable se prosljeđuju proceduri getTwoRandom kao stvarni parametri: rand1 i rand2. Treći stvarni parametar (10) se prosljeđuje po vrijednosti. Procedura piše koristeći formalne parametre

Metode programiranja koristeći stringove

Svrha laboratorijskog rada : naučiti metode u jeziku C#, pravila za rad sa znakovnim podacima i komponentu ListBox. Napišite program za rad sa stringovima.

Metode

Metoda je element klase koji sadrži programski kod. Metoda ima sledeću strukturu:

[atributi] [specifikatori] naziv tipa ([parametri])

Tijelo metode;

Atributi su posebna uputstva kompajleru o svojstvima metode. Atributi se rijetko koriste.

Kvalifikatori su ključne riječi koje služe različitim svrhama, na primjer:

· Određivanje dostupnosti metode za druge klase:

o privatni– metoda će biti dostupna samo unutar ove klase

o zaštićeno– metoda će biti dostupna i dječijoj nastavi

o javnosti– metoda će biti dostupna bilo kojoj drugoj klasi koja može pristupiti ovoj klasi

Označavanje dostupnosti metode bez kreiranja klase

· Vrsta postavke

Tip određuje rezultat koji metoda vraća: to može biti bilo koji tip dostupan u C#, kao i ključna riječ void ako rezultat nije potreban.

Ime metode je identifikator koji će se koristiti za pozivanje metode. Za identifikator se primjenjuju isti zahtjevi kao i za imena varijabli: može se sastojati od slova, brojeva i donje crte, ali ne može početi brojem.

Parametri su lista varijabli koje se mogu proslijediti metodi kada se pozove. Svaki parametar se sastoji od tipa varijable i imena. Parametri su odvojeni zarezima.

Tijelo metode je normalan programski kod, osim što ne može sadržavati definicije drugih metoda, klasa, imenskih prostora, itd. Ako metoda mora vratiti neki rezultat, tada ključna riječ return mora biti prisutna na kraju sa povratnom vrijednošću. . Ako vraćanje rezultata nije potrebno, onda upotreba ključne riječi return nije potrebna, iako je dozvoljena.

Primjer metode koja procjenjuje izraz:

javni dupli izračun (dvostruki a, dupli b, dupli c)

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

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

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

Preopterećenje metode

Jezik C# vam omogućava da kreirate više metoda sa istim imenima, ali različitim parametrima. Kompajler će automatski odabrati najprikladniji metod prilikom izrade programa. Na primjer, možete napisati dvije odvojene metode za podizanje broja na stepen: jedan algoritam bi se koristio za cijele brojeve, a drugi bi se koristio za realne brojeve:

///

/// Izračunaj X na stepen Y za cijele brojeve

///

privatni int Pow(int X, int Y)

///

/// Izračunajte X na stepen Y za realne brojeve

///

privatni dupli Pow (dvostruki X, dupli Y)

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

inače ako (Y == 0)

Ovaj kod se zove na isti način, jedina razlika je u parametrima - u prvom slučaju kompajler će pozvati metodu Pow sa cjelobrojnim parametrima, au drugom - sa stvarnim parametrima:

Default Settings

Jezik C#, počevši od verzije 4.0 (Visual Studio 2010), omogućava vam da postavite zadane vrijednosti za neke parametre, tako da prilikom pozivanja metode možete izostaviti neke od parametara. Da biste to učinili, prilikom implementacije metode, potrebnim parametrima treba dodijeliti vrijednost direktno u listi parametara:

private void GetData(int Broj, int Opciono = 5 )

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

Console.WriteLine("Opcionalno: (0)", Opciono);

U ovom slučaju, metodu možete pozvati na sljedeći način:

GetData(10, 20);

U prvom slučaju, Opcioni parametar će biti jednak 20, pošto je eksplicitno naveden, au drugom slučaju će biti jednak 5, jer nije eksplicitno specificirano i kompajler uzima zadanu vrijednost.

Zadani parametri se mogu postaviti samo na desnoj strani liste parametara; na primjer, takav potpis metode neće biti prihvaćen od strane kompajlera:

private void GetData(int Opciono = 5 , int broj)

Kada se parametri prosleđuju metodi na normalan način (bez dodatnih ključnih reči ref i out), sve promene parametara unutar metode ne utiču na njenu vrednost u glavnom programu. Recimo da imamo sljedeću metodu:

privatni void Calc (int Number)

Vidi se da se unutar metode mijenja varijabla Number, koja je proslijeđena kao parametar. Pokušajmo pozvati metodu:

Console.WriteLine(n);

Na ekranu će se pojaviti broj 1, odnosno, uprkos promjeni varijable u Calc metodi, vrijednost varijable u glavnom programu se nije promijenila. To je zbog činjenice da kada se metoda pozove, a kopija proslijeđena varijabla, to je ta varijabla koju metoda mijenja. Kada se metoda završi, vrijednost kopija se gubi. Ova metoda prosljeđivanja parametra se zove proći po vrijednosti.

Da bi metoda promijenila varijablu koja joj je proslijeđena, ona mora biti proslijeđena ključnom riječi ref - mora biti i u potpisu metode i kada je pozvana:

privatni void Calc (ref int broj)

Console.WriteLine(n);

U tom slučaju, na ekranu će se pojaviti broj 10: promjena vrijednosti u metodi utjecala je i na glavni program. Ovaj metod prenosa se zove prolazeći po referenci, tj. Više se ne prenosi kopija, već referenca na stvarnu varijablu u memoriji.

Ako metoda koristi varijable po referenci samo za vraćanje vrijednosti i nije je briga što je u njima u početku, onda ne možete inicijalizirati takve varijable, već ih proslijediti s ključnom riječi out. Kompajler razumije da početna vrijednost varijable nije važna i ne žali se na nedostatak inicijalizacije:

privatni void Calc (out int Number)

int n; // Mi ništa ne dodjeljujemo!

string tip podataka

Jezik C# koristi tip stringa za pohranjivanje stringova. Da biste deklarirali (i, po pravilu, odmah inicijalizirali) varijablu niza, možete napisati sljedeći kod:

string a = "Tekst";

string b = "strings";

Možete izvršiti operaciju sabiranja na linijama - u ovom slučaju, tekst jednog retka bit će dodan tekstu drugog:

string c = a + " " + b; // Rezultat: String tekst

Tip stringa je zapravo pseudonim za klasu String, koja vam omogućava da izvršite niz složenijih operacija nad stringovima. Na primjer, metoda IndexOf može tražiti podniz u nizu, a metoda Substring vraća dio niza određene dužine, počevši od određene pozicije:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int indeks = a.IndexOf("OP"); // Rezultat: 14 (računajući od 0)

string b = a.Podniz(3, 5); // Rezultat: DEFGH

Ako trebate dodati posebne znakove u niz, to možete učiniti pomoću escape sekvenci koje počinju s obrnutom kosom crtom:

ListBox Component

Komponenta ListBox je lista čiji se elementi biraju pomoću tastature ili miša. Lista elemenata je određena svojstvom Predmeti. Stavke su element koji ima svoja svojstva i svoje metode. Metode Dodati, RemoveAt I Insert koriste se za dodavanje, brisanje i umetanje elemenata.

Objekt Predmeti pohranjuje objekte na listi. Objekt može biti bilo koja klasa - podaci klase se konvertuju za prikaz u string reprezentaciju pomoću ToString metode. U našem slučaju, nizovi će se ponašati kao objekti. Međutim, budući da objekt Items pohranjuje objekte pretvorene u objekt tipa, prije upotrebe morate ih vratiti na njihov originalni tip, u našem slučaju niz:

string a = (string)listBox1.Items;

Da biste odredili broj odabranog elementa, koristite svojstvo SelectedIndex.

Kada sam počeo da programiram na C++ i intenzivno proučavam knjige i članke, uvek sam nailazio na isti savet: ako treba da prosledimo neki objekat funkciji koji ne bi trebalo da se menja u funkciji, onda ga uvek treba prosleđivati pozivanjem na konstantu(PPSK), osim u slučajevima kada trebamo proslijediti primitivni tip ili strukturu sličnu njima. Jer Za više od 10 godina programiranja u C++-u, vrlo često sam nailazio na ovaj savjet (i sam sam ga dao više puta), on je odavno "upijen" u mene - automatski prosljeđujem sve argumente upućivanjem na konstantu . Ali vrijeme prolazi i već je prošlo 7 godina otkako smo imali na raspolaganju C++11 sa njegovom semantikom poteza, u vezi s tim čujem sve više glasova koji preispituju dobru staru dogmu. Mnogi počinju da tvrde da je prelazak na konstantu stvar prošlosti i da je sada neophodno proći po vrijednosti(PPZ). Šta se krije iza ovih razgovora, kao i koje zaključke možemo izvući iz svega ovoga, želim da prodiskutujem u ovom članku.

Knjižna mudrost

Da bismo shvatili kakvog se pravila trebamo pridržavati, predlažem da se okrenemo knjigama. Knjige su odličan izvor informacija koje nismo dužni prihvatiti, ali koje svakako vrijedi poslušati. I počećemo od istorije, od porekla. Neću saznati ko je bio prvi apologeta PPSC-a, samo ću kao primjer navesti knjigu koja je na mene lično imala najveći utjecaj po pitanju korištenja PPSC-a.

Mayers

Dobro, ovdje imamo klasu u kojoj se svi parametri prosljeđuju referencom, ima li problema sa ovom klasom? Nažalost, postoji, a ovaj problem leži na površini. U našoj klasi imamo 2 funkcionalna entiteta: prvi uzima vrijednost u fazi kreiranja objekta, a drugi vam omogućava da promijenite prethodno postavljenu vrijednost. Imamo dva entiteta, ali četiri funkcije. Sada zamislite da ne možemo imati 2 slična entiteta, već 3, 5, 6, šta onda? Tada ćemo se suočiti sa ozbiljnim naduvavanjem koda. Stoga, kako se ne bi stvorila masa funkcija, postojao je prijedlog da se veze u parametrima potpuno napuste:

Predložak klasa Holder (javno: eksplicitno Holder(T vrijednost): m_Value(move(value)) ( ) void setValue(T value) (m_Value = move(value); ) const T& value() const noexcept (vraćanje m_Value;) privatno: T m_Value; );

Prva prednost koja vam odmah upada u oči je da ima znatno manje koda. Ima ga čak i manje nego u prvoj verziji, zbog uklanjanja const i & (iako su dodali move ). Ali oduvijek su nas učili da je prosljeđivanje po referenci produktivnije od prosljeđivanja po vrijednosti! Ovako je bilo prije C++11, i kako je i dalje, ali sada ako pogledamo ovaj kod, vidjet ćemo da ovdje nema više kopiranja nego u prvoj verziji, pod uslovom da T ima konstruktor poteza. One. Sam PPSC je bio i biće brži od PPZ-a, ali kod na neki način koristi prosleđenu referencu i često se ovaj argument kopira.

Međutim, ovo nije cijela priča. Za razliku od prve opcije, gdje imamo samo kopiranje, ovdje dodajemo i kretanje. Ali selidba je jeftina operacija, zar ne? Na ovu temu, Mayersova knjiga koju razmatramo ima i poglavlje („Stavka 29“) koje nosi naslov: „Pretpostavimo da operacije kretanja nisu prisutne, da nisu jeftine i da se ne koriste.“ Glavna ideja bi trebala biti jasna iz naslova, ali ako želite detalje, svakako je pročitajte - neću se zadržavati na tome.

Ovdje bi bilo prikladno provesti potpunu komparativnu analizu prve i posljednje metode, ali ne bih želio odstupiti od knjige, pa ćemo analizu odložiti za druge dijelove, a ovdje ćemo nastaviti razmatrati Scottove argumente. Dakle, osim činjenice da je treća opcija očito kraća od druge, u čemu Scott vidi prednost PPZ-a nad PPSC-om u modernom kodu?

On to vidi u činjenici da u slučaju donošenja rvalue, tj. neki zovu ovako: Holder holder(string("me")); , opcija sa PPSC će nam dati kopiranje, a opcija sa PPZ će nam dati kretanje. S druge strane, ako je transfer ovakav: Holder holder(someLvalue); , onda PPZ definitivno gubi zbog činjenice da će vršiti i kopiranje i premještanje, dok će u verziji sa PPSC biti samo jedno kopiranje. One. ispada da je PPZ, ako uzmemo u obzir čisto efikasnost, neka vrsta kompromisa između količine koda i “pune” (preko && ) podrške za semantiku kretanja.

Zato je Scott tako pažljivo formulirao svoj savjet i tako pažljivo ga promovira. Čak mi se činilo da je to iznio nerado, kao pod pritiskom: nije mogao a da ne unese rasprave o ovoj temi u knjizi, jer... raspravljalo se prilično naširoko, a Scott je uvijek bio sakupljač kolektivnog iskustva. Uz to, on iznosi vrlo malo argumenata u odbranu PPZ-a, ali daje dosta onih koji ovu “tehniku” dovode u pitanje. Njegove argumente protiv ćemo pogledati u kasnijim odjeljcima, ali ovdje ćemo ukratko ponoviti argument koji Scott iznosi u odbranu JPP-a (mentalno dodajući “ako predmet podržava kretanje i jeftin je”): omogućava vam da izbjegnete kopiranje kada prosljeđujete izraz rvalue kao argument funkcije. Ali dosta mučenja Meyersove knjige, pređimo na drugu knjigu.

Usput, ako je neko pročitao knjigu i iznenađen je što ovdje ne uključujem opciju s onim što je Mayers nazvao univerzalnim referencama – sada poznatim kao reference za prosljeđivanje – onda je to lako objasniti. Razmišljam samo o PPZ i PPSC, jer... Smatram da je loša forma uvesti funkcije šablona za metode koje nisu šabloni samo radi podrške prosleđivanja po referenci oba tipa (rvalue/lvalue). Da ne spominjemo činjenicu da kod ispada drugačiji (nema više postojanosti) i sa sobom nosi druge probleme.

Josattis i društvo

Posljednja knjiga koju ćemo pogledati je “C++ predlošci”, koja je ujedno i najnovija od svih knjiga spomenutih u ovom članku. Objavljena je krajem 2017. (a 2018. je naznačena u knjizi). Za razliku od drugih knjiga, ova je u potpunosti posvećena obrascima, a ne savjetima (kao Mayers) ili C++ općenito, kao Stroustrup. Stoga se ovdje razmatraju prednosti/protiv sa stanovišta pisanja šablona.

Ovoj temi je posvećeno čitavo 7. poglavlje, koje ima elokventan naslov “Po vrijednosti ili po referenci?”. U ovom poglavlju, autori prilično kratko, ali sažeto opisuju sve metode prijenosa sa svim njihovim prednostima i nedostacima. Analiza efikasnosti ovdje se praktično ne daje, a podrazumijeva se da će PPSC biti brži od PPZ-a. Ali uz sve ovo, na kraju poglavlja autori preporučuju korištenje zadanog PPP-a za funkcije šablona. Zašto? Budući da se korištenjem veze parametri šablona prikazuju u potpunosti, a bez veze se „propadaju“, što povoljno djeluje na obradu nizova i literala niza. Autori vjeruju da ako se za neku vrstu PPP-a ispostavi da je neefikasan, onda uvijek možete koristiti std::ref i std::cref. Ovo je savjet, da budem iskren, jeste li vidjeli mnogo ljudi koji žele koristiti gore navedene funkcije?

Šta oni savjetuju u vezi sa PPSC? Oni savjetuju korištenje PPSC-a kada su performanse kritične ili postoje druge težak razlozi da se ne koristi PPP. Naravno, ovdje govorimo samo o šablonskom kodu, ali ovaj savjet je u direktnoj suprotnosti sa svime čemu su programeri učili deceniju. Ovo nije samo savjet da se JPP smatra alternativom – ne, ovo je savjet da PPSC postane alternativa.

Ovim završavamo našu turneju knjige, jer... Ne znam ni za jednu drugu knjigu koju bismo trebali konsultovati o ovom pitanju. Pređimo na drugi medijski prostor.

Mrežna mudrost

Jer Živimo u doba interneta, onda se ne treba oslanjati samo na mudrost knjige. Štaviše, mnogi autori koji su nekada pisali knjige sada jednostavno pišu blogove i napustili su knjige. Jedan od tih autora je Herb Sutter, koji je u maju 2013. godine objavio članak na svom blogu “GotW #4 Solution: Class Mechanics”, koji, iako nije u potpunosti posvećen problemu koji pokrivamo, ipak ga se dotiče.

Dakle, u originalnoj verziji članka, Sutter je jednostavno ponovio staru mudrost: „predaj parametre prema konstanti“, ali ovu verziju članka više nećemo vidjeti, jer Članak sadrži suprotan savjet: „ Ako parametar će i dalje biti kopiran, a zatim ga proslijedite po vrijednosti.” Opet ozloglašeno "ako". Zašto je Sutter promijenio članak i kako sam ja znao za to? Iz komentara. Pročitajte komentare na njegov članak, oni su, inače, zanimljiviji i korisniji od samog članka. Istina, nakon što je napisao članak, Sutter je konačno promijenio mišljenje i više ne daje takve savjete. Promjenu mišljenja može se naći u njegovom govoru na CppCon-u 2014. godine: „Nazad na osnove! Osnove modernog C++ stila". Obavezno pogledajte, preći ćemo na sljedeći internet link.

I dalje imamo glavni programski resurs 21. veka: StackOverflow. Tačnije odgovor, s brojem pozitivnih reakcija premašivši 1700 u vrijeme pisanja ovog članka. Pitanje je: Šta je idiom kopiraj i zamijeni? , i, kao što bi naslov trebao sugerirati, nije baš u vezi s temom koju gledamo. Ali u svom odgovoru na ovo pitanje autor se dotiče i teme koja nas zanima. Također savjetuje korištenje PPZ-a „ako će argument ipak biti kopiran“ (vrijeme je da se uvede i skraćenica za ovo, bogami). I generalno, ovaj savjet se čini sasvim primjerenim, u okviru njegovog odgovora i operatora o kojem se tu govori, ali autor uzima slobodu da takav savjet daje na širi način, a ne samo u ovom konkretnom slučaju. Štaviše, on ide dalje od svih savjeta o kojima smo prethodno razgovarali i poziva na to čak i u C++03 kodu! Šta je navelo autora na takve zaključke?

Očigledno, autor odgovora je crpio glavnu inspiraciju iz članka drugog autora knjige i honorarnog programera Boost.MPL-a - Davea Abrahamsa. Članak se zove „Želiš brzinu? Prođite pored vrijednosti.” , a objavljen je još u avgustu 2009. godine, tj. 2 godine prije usvajanja C++11 i uvođenja semantike pokreta. Kao iu prethodnim slučajevima, preporučujem čitatelju da sam pročita članak, ali ja ću navesti glavne argumente (postoji, zapravo, samo jedan argument) koje Dave navodi u korist PPZ-a: morate koristiti PPZ , jer optimizacija “skip copy” dobro radi s njim (copy elision), koja je odsutna u PPSC-u. Ako pročitate komentare na članak, možete vidjeti da savjeti koje promoviše nisu univerzalni, što potvrđuje i sam autor odgovarajući na kritike komentatora. Međutim, članak sadrži eksplicitne savjete (smjernice) za korištenje PPP-a ako će argument ipak biti kopiran. Inače, ako nekoga zanima, možete pročitati članak „Želite brzinu? Nemojte (uvijek) zaobići vrijednost.” . Kao što naslov treba da kaže, ovaj članak je odgovor na Daveov članak, pa ako pročitate prvi, svakako pročitajte i ovaj!

Nažalost (na sreću nekih), ovakvi članci i (još više) popularni odgovori na popularnim stranicama dovode do masovne upotrebe sumnjivih tehnika (trivijalan primjer) jednostavno zato što to zahtijeva manje pisanja, a stara dogma više nije nepokolebljiva - Uvijek se možete pozvati na „taj popularni savjet“ ako ste gurnuti uza zid. Sada predlažem da se upoznate sa onim što nam različiti resursi nude sa preporukama za pisanje koda.

Jer Budući da se razni standardi i preporuke sada objavljuju i na internetu, odlučio sam ovaj odjeljak klasificirati kao „mrežna mudrost“. Dakle, ovde bih želeo da govorim o dva izvora, čija je svrha da poboljšaju kod C++ programera pružajući im savete (smernice) kako da napišu baš ovaj kod.

Prvi skup pravila koji želim da razmotrim bio je posljednja kap koja me je natjerala da uzmem ovaj članak. Ovaj skup je dio uslužnog programa clang-tidy i ne postoji izvan njega. Kao i sve što je vezano za clang, ovaj uslužni program je veoma popularan i već je dobio integraciju sa CLion i Resharper C++ (tako sam naišao). Dakle, clang-tydy sadrži pravilo modernize-pass-by-value koje radi na konstruktorima koji prihvataju argumente preko PPSC-a. Ovo pravilo sugerira da zamijenimo PPSC sa PPZ. Štaviše, u vrijeme pisanja članka, opis ovog pravila sadrži napomenu da je ovo pravilo ćao radi samo za konstruktore, ali će oni (tko su oni?) rado prihvatiti pomoć od onih koji proširuju ovo pravilo na druge entitete. Tamo, u opisu, postoji poveznica na Daveov članak - jasno je odakle dolaze noge.

Na kraju, da zaključimo ovaj pregled tuđe mudrosti i autoritativnih mišljenja, predlažem da pogledate zvanične smjernice za pisanje C++ koda: C++ Core Guidelines, čiji su glavni urednici Herb Sutter i Bjarne Stroustrup (nije loše, zar ne?). Dakle, ove preporuke sadrže sljedeće pravilo: “Za “in” parametre, proslijedite jeftino kopirane tipove po vrijednosti, a ostale po referenci na const”, što u potpunosti ponavlja staru mudrost: PPSK svuda i PPP za male objekte. Ovaj savjet navodi nekoliko alternativa koje treba razmotriti. u slučaju da je prenošenju argumenata potrebna optimizacija. Ali PPZ nije uvršten na listu alternativa!

Pošto nemam drugih izvora vrijednih pažnje, predlažem da pređemo na direktnu analizu oba načina prijenosa.

Analiza

Cijeli prethodni tekst je napisan na za mene pomalo neuobičajen način: iznosim tuđa mišljenja i čak se trudim da ne iznosim svoje (znam da ispada loše). Najviše zbog činjenice da su mišljenja drugih, a cilj mi je bio da ih ukratko sagledam, odložio sam detaljno razmatranje pojedinih argumenata koje sam našao kod drugih autora. U ovom dijelu se neću pozivati ​​na autoritete i davati mišljenja, ovdje ćemo se osvrnuti na neke objektivne prednosti i mane PPSC i PPZ, koje će biti začinjene mojom subjektivnom percepcijom. Naravno, ponovit će se nešto od onoga o čemu je ranije bilo riječi, ali, nažalost, ovo je struktura ovog članka.

Ima li JPP prednost?

Dakle, prije razmatranja argumenata za i protiv, predlažem da pogledamo šta i u kojim slučajevima nam daje prednost koju nam daje prolazak pored vrijednosti. Recimo da imamo klasu poput ove:

Class CopyMover (javno: void setByValuer(Accounter byValuer) (m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) (m_ByRefer = byRefer; ) void setByValuverAluer(byValuer) yValuerAndNotMover = byVal uerAndNotMover; ) void setRvaluer (Račun&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Iako nas za potrebe ovog članka zanimaju samo prve dvije funkcije, uključio sam četiri opcije samo da ih koristim kao kontrast.

Klasa Accounter je jednostavna klasa koja broji koliko je puta kopirana/premeštena. A u klasi CopyMover implementirali smo funkcije koje nam omogućavaju da razmotrimo sljedeće opcije:

    kreće se prošao argument.

    Prođite po vrijednosti, praćeno kopiranje prošao argument.

Sada, ako prosledimo lvalue svakoj od ovih funkcija, na primer ovako:

Accounter byRefer; Accounter byValuer; Računovođa byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

tada dobijamo sljedeće rezultate:

Očigledni pobjednik je PPSC, jer... daje samo jedan primjerak, dok PPZ daje jedan primjerak i jedan potez.

Pokušajmo sada proslijediti rvalue:

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

Dobijamo sljedeće:

Ovdje nema jasnog pobjednika, jer... i PPZ i PPSK imaju po jednu operaciju, ali zbog činjenice da PPZ koristi kretanje, a PPSK kopiranje, možemo dati pobjedu PPZ-u.

Ali naši eksperimenti se tu ne završavaju; dodajmo sljedeće funkcije za simulaciju indirektnog poziva (sa naknadnim prosljeđivanjem argumenta):

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

Koristićemo ih na potpuno isti način kao i bez njih, tako da neću ponavljati kod (pogledajte u spremište ako je potrebno). Dakle, za lvalue rezultati bi bili ovakvi:

Imajte na umu da PPSC povećava jaz u odnosu na PPZ, ostajući sa jednom kopijom, dok PPZ već ima čak 3 operacije (jedan pokret više)!

Sada prenosimo rvalue i dobijamo sljedeće rezultate:

Sada PPZ ima 2 pokreta, a PPSC još uvijek ima jedan primjerak. Da li je sada moguće nominirati PPZ za pobjednika? Ne, jer ako bi jedan potez trebao biti barem ne gori od jedne kopije, ne možemo reći isto za 2 poteza. Dakle, u ovom primjeru neće biti pobjednika.

Mogu mi prigovoriti: „Autore, ti imaš pristrasno mišljenje i uvlačiš ono što ti je od koristi. Čak i 2 poteza će biti jeftinije od kopiranja!” Ne mogu se složiti sa ovom izjavom Sve u svemu, jer Koliko je brže kretanje od kopiranja zavisi od specifične klase, ali „jeftino“ premještanje ćemo pogledati u posebnom odjeljku.

Ovdje smo se dotakli zanimljive stvari: dodali smo jedan indirektni poziv, a PPP je dodao tačno jednu operaciju u “težini”. Mislim da ne morate imati diplomu MSTU-a da biste shvatili da što više indirektnih poziva imamo, to će se više operacija obavljati pri korištenju PPZ-a, dok će za PPSC broj ostati nepromijenjen.

Sve o čemu smo gore govorili nije vjerovatno bilo kome otkriće, možda nismo ni provodili eksperimente - svi ovi brojevi bi većini C++ programera trebali biti očigledni na prvi pogled. Istina, jedna stvar ipak zaslužuje pojašnjenje: zašto, u slučaju rvalue, PZ nema kopiju (ili drugi potez), već samo jedan potez.

Pa, pogledali smo razliku u prijenosu između PPZ i PPSC posmatrajući iz prve ruke broj kopija i poteza. Iako je očito da je prednost PPZ-a nad PPSC-om čak i u ovako jednostavnim primjerima, blago rečeno Ne Očigledno, još uvijek, pomalo pretenciozno, donosim sljedeći zaključak: ako ćemo i dalje kopirati argument funkcije, onda ima smisla razmotriti prosljeđivanje argumenta funkciji po vrijednosti. Zašto sam izveo ovaj zaključak? Za glatko prelazak na sljedeći odjeljak.

Ako kopiramo...

Dakle, dolazimo do poslovičnog „ako“. Većina argumenata na koje smo naišli nisu pozivali na univerzalnu implementaciju PPP-a umjesto PPSC-a; samo su pozivali na to „ako se argument ipak kopira“. Vrijeme je da shvatimo šta nije u redu sa ovim argumentom.

Želim da počnem sa malim opisom kako pišem kod. U posljednje vrijeme moj proces kodiranja sve više liči na TDD, tj. pisanje bilo koje metode klase počinje pisanjem testa u kojem se ova metoda pojavljuje. Shodno tome, kada počinjem da pišem test, i kreiram metodu nakon pisanja testa, još uvek ne znam da li ću kopirati argument. Naravno, nisu sve funkcije kreirane na ovaj način; često, čak iu procesu pisanja testa, tačno znate kakva će biti implementacija. Ali to se ne dešava uvek!

Neko bi mi mogao prigovoriti da nije bitno kako je metoda originalno napisana, možemo promijeniti način na koji prosljeđujemo argument kada se metoda uobliči i da nam je potpuno jasno šta se tu dešava (tj. da li imamo kopiranje ili ne ). Delimično se slažem sa ovim – zaista, možete to da uradite na ovaj način, ali to nas uključuje u neku vrstu čudne igre u kojoj moramo da menjamo interfejse samo zato što se promenila implementacija. Što nas dovodi do sljedeće dileme.

Ispostavilo se da modificiramo (ili čak planiramo) interfejs na osnovu toga kako će biti implementiran. Ne smatram se stručnjakom za OOP i druge teorijske proračune softverske arhitekture, ali takve radnje su jasno u suprotnosti sa osnovnim pravilima kada implementacija ne bi trebala utjecati na sučelje. Naravno, određeni detalji implementacije (bilo da su u pitanju karakteristike jezika ili ciljne platforme) i dalje cure kroz interfejs na ovaj ili onaj način, ali treba pokušati smanjiti, a ne povećati broj takvih stvari.

Pa, Bog ga blagoslovio, hajde da krenemo ovim putem i da ipak menjamo interfejse u zavisnosti od toga šta radimo u implementaciji u smislu kopiranja argumenta. Recimo da smo napisali ovu metodu:

Void setName(ime ime) (m_Name = potez(ime); )

i urezao naše promjene u spremište. Kako je vrijeme prolazilo, naš softverski proizvod je dobijao nove funkcionalnosti, integrisali su se novi okviri i postavljao se zadatak informiranja vanjskog svijeta o promjenama u našoj klasi. One. Našoj metodi ćemo dodati neki mehanizam obavještavanja, neka bude nešto slično Qt signalima:

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

Postoji li problem sa ovim kodom? Jedi. Za svaki poziv setName šaljemo signal, tako da će signal biti poslan čak i kada značenje m_Name nije promijenjeno. Osim problema s performansama, ova situacija može dovesti do beskonačne petlje zbog koda koji primi gornje obavještenje na neki način da pozove setName . Kako biste izbjegli sve ove probleme, takve metode najčešće izgledaju otprilike ovako:

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

Riješili smo se gore opisanih problema, ali sada naše pravilo “ako ipak kopiramo...” nije uspjelo - više nema bezuvjetnog kopiranja argumenta, sada ga kopiramo samo ako se promijeni! Pa šta da radimo sada? Promijeniti interfejs? Ok, hajde da promenimo interfejs klase zbog ovog popravka. Šta ako je naša klasa naslijedila ovu metodu od nekog apstraktnog interfejsa? Promijenimo i tamo! Ima li puno promjena jer se promijenila implementacija?

Opet mi mogu prigovoriti, kažu, autoru, zašto pokušavaš da uštediš na utakmicama kada će taj uslov funkcionisati tamo? Da, većina poziva će biti lažna! Ima li povjerenja u ovo? Gdje? A ako sam odlučio uštedjeti na utakmicama, nije li sama činjenica da smo koristili PPZ bila posljedica upravo takve uštede? Ja samo nastavljam "stranačku liniju" koja se zalaže za efikasnost.

Konstruktori

Idemo ukratko preko konstruktora, pogotovo jer za njih postoji posebno pravilo u clang-tidy, koje još ne radi za druge metode/funkcije. Recimo da imamo ovakvu klasu:

Klasa JustClass (javno: JustClass(const string& justString): m_JustString(justString) () privatno: string m_JustString; );

Očigledno, parametar je kopiran, a clang-tidy će nam reći da bi bilo dobro prepisati konstruktor na ovo:

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

I, iskreno govoreći, teško mi je ovdje raspravljati - na kraju krajeva, mi zaista uvijek kopiramo. I najčešće, kada nešto prođemo kroz konstruktor, mi to kopiramo. Ali češće ne znači uvijek. Evo još jednog primjera:

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

Ovdje ne kopiramo uvijek, već samo kada su datumi prikazani ispravno. Naravno, u velikoj većini slučajeva to će biti slučaj. Ali nije uvijek.

Možete dati još jedan primjer, ali ovaj put bez koda. Zamislite da imate klasu koja prihvata veliki objekat. Klasa postoji dugo vremena, a sada je vrijeme da se ažurira njena implementacija. Shvaćamo da nam ne treba više od polovine velikog objekta (koji je godinama rastao), a možda i manje. Možemo li učiniti nešto po tom pitanju tako što ćemo proći po vrijednosti? Ne, nećemo moći ništa učiniti, jer će kopija i dalje biti kreirana. Ali kada bismo koristili PPSC, jednostavno bismo promijenili ono što radimo unutra dizajner. A ovo je ključna stvar: koristeći PPSC mi kontroliramo šta i kada se događa u implementaciji naše funkcije (konstruktor), ali ako koristimo PPZ, onda gubimo svaku kontrolu nad kopiranjem.

Šta možete uzeti iz ovog odjeljka? Činjenica da je argument “ako ipak prepišemo...” vrlo kontroverzan, jer Ne znamo uvijek šta ćemo kopirati, a čak i kada znamo, vrlo često nismo sigurni da će se tako nastaviti i u budućnosti.

Selidba je jeftina

Od samog trenutka kada se pojavila semantika pokreta, počela je ozbiljno da utiče na način pisanja modernog C++ koda, a vremenom se taj uticaj samo pojačavao: nije ni čudo, jer je kretanje tako jeftino u poređenju sa kopiranjem. Ali je li? Da li je istina da je kretanje Uvijek jeftina operacija? To je ono što ćemo pokušati da shvatimo u ovom odeljku.

Binarni veliki objekt

Počnimo s trivijalnim primjerom, recimo da imamo sljedeću klasu:

Struct Blob ( std::array podaci; );

Obicno mrlja(BDO, engleski BLOB), koji se može koristiti u raznim situacijama. Pogledajmo koliko će nas koštati prolazak po referenci i po vrijednosti. Naš BDO će se koristiti otprilike ovako:

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

A mi ćemo ove funkcije nazvati ovako:

Const Blob blob(); skladištenje; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Kod za ostale primjere će biti identičan ovom, samo s različitim imenima i tipovima, tako da ga neću davati za preostale primjere - sve je u spremištu.

Prije nego što pređemo na mjerenja, pokušajmo predvidjeti rezultat. Dakle, imamo 4 KB std::array koji želimo pohraniti u objekt klase Storage. Kako smo ranije saznali, za PPSC ćemo imati jedan primjerak, dok ćemo za PPZ imati jedan primjerak i jedan potez. Na osnovu činjenice da je nemoguće premjestiti niz, biće 2 kopije za PPZ, naspram jedne za PPSC. One. možemo očekivati ​​dvostruku superiornost u performansama za PPSC.

Sada pogledajmo rezultate testa:

Ovaj i svi naredni testovi su izvedeni na istoj mašini koristeći MSVS 2017 (15.7.2) i /O2 zastavicu.

Praksa se poklopila s pretpostavkom - prenošenje po vrijednosti je 2 puta skuplje, jer je za niz premještanje potpuno ekvivalentno kopiranju.

Linija

Pogledajmo još jedan primjer, običan std::string. Šta možemo očekivati? Znamo (o tome sam raspravljao u članku) da moderne implementacije razlikuju dvije vrste stringova: kratke (oko 16 znakova) i dugačke (one koje su duže od kratke). Za kratke, koristi se interni bafer, koji je običan C-niz char, ali dugi će već biti postavljeni na hrpu. Kratki redovi nas ne zanimaju, jer... rezultat će tamo biti isti kao kod BDO-a, pa se fokusirajmo na dugačke redove.

Dakle, imajući dugačak niz, očito je da bi njegovo pomicanje trebalo biti prilično jeftino (samo pomjerite pokazivač), tako da možete računati na činjenicu da pomicanje niza uopće ne bi trebalo utjecati na rezultate, a PPZ bi trebao dati rezultat nije gori od PPSC-a. Provjerimo to u praksi i dobijemo sljedeće rezultate:

Preći ćemo na objašnjenje ovog “fenomena”. Dakle, šta se dešava kada kopiramo postojeći string u već postojeći string? Pogledajmo trivijalan primjer:

Prvi niz (64, "C"); string second(64, "N"); //... drugi = prvi;

Imamo dva niza od 64 karaktera, tako da interni bafer nije dovoljan prilikom njihovog kreiranja, što rezultira da su oba niza dodijeljena na hrpi. Sada kopiramo prvo u drugo. Jer naše veličine redova su iste, očito ima dovoljno prostora dodijeljenog u drugom za smještaj svih podataka iz prvog, tako da drugi = prvi; bit će banalan memcpy, ništa više. Ali ako pogledamo malo izmijenjen primjer:

Prvi niz (64, "C"); niz drugi = prvi;

tada više neće biti poziva operator= , već će biti pozvan konstruktor kopiranja. Jer Pošto imamo posla sa konstruktorom, u njemu nema postojeće memorije. Prvo se mora odabrati pa tek onda prvo kopirati. One. ovo je dodjela memorije, a zatim memcpy. Kao što vi i ja znamo, dodjela memorije na globalnoj hrpi obično je skupa operacija, tako da će kopiranje iz drugog primjera biti skuplje od kopiranja iz prvog. Skuplja alokacija memorije po hrpi.

Kakve to veze ima sa našom temom? Najdirektniji, jer prvi primjer pokazuje šta se tačno dešava sa PPSC, a drugi pokazuje šta se dešava sa PPZ: za PPZ se uvek kreira novi red, dok se za PPSC ponovo koristi postojeći. Već ste vidjeli razliku u vremenu izvršenja, tako da se ovdje nema šta dodati.

I ovdje smo suočeni s činjenicom da prilikom korištenja JPP-a radimo van konteksta, te stoga ne možemo iskoristiti sve pogodnosti koje ono može pružiti. I ako smo ranije razmišljali u smislu teoretskih budućih promjena, ovdje uočavamo vrlo konkretan pad u produktivnosti.

Naravno, neko bi mi mogao prigovoriti da se nizovi izdvajaju, a većina tipova ne funkcionira na taj način. Na što mogu odgovoriti sljedeće: sve što je ranije opisano bit će istinito za bilo koji kontejner koji dodjeljuje memoriju u hrpu odmah za paket elemenata. Također, ko zna koje se druge kontekstno osjetljive optimizacije koriste u drugim tipovima?

Šta biste trebali uzeti iz ovog odjeljka? Činjenica da čak i ako je premještanje zaista jeftino ne znači da će zamjena kopiranja kopiranjem+premještanjem uvijek dati rezultat uporediv u smislu performansi.

Kompleksni tip

Na kraju, pogledajmo tip koji će se sastojati od više objekata. Neka ovo bude klasa Person, koja se sastoji od podataka svojstvenih osobi. Obično je to vaše ime, prezime, poštanski broj itd. Sve ovo možete predstaviti kao nizove i pretpostaviti da će stringovi koje stavite u polja klase Person vjerovatno biti kratki. Iako vjerujem da će u stvarnom životu mjerenje kratkih žica biti najkorisnije, ipak ćemo gledati žice različitih veličina kako bismo dali potpuniju sliku.

Također ću koristiti Person sa 10 polja, ali za ovo neću kreirati 10 polja direktno u tijelu klase. Implementacija Persona skriva kontejner u svojim dubinama - to ga čini pogodnijim za promjenu parametara testa, praktično bez odstupanja od načina na koji bi funkcionirao da je Person prava klasa. Međutim, implementacija je dostupna i uvijek možete provjeriti kod i reći mi jesam li učinio nešto pogrešno.

Dakle, idemo: Osoba sa 10 polja tipa string, koje prenosimo pomoću PPSC i PPZ u Storage:

Kao što vidite, imamo ogromnu razliku u performansama, što čitaoce nakon prethodnih odeljaka ne bi trebalo da iznenadi. Također vjerujem da je klasa Person dovoljno "stvarna" da takvi rezultati neće biti odbačeni kao apstraktni.

Inače, kada sam pripremao ovaj članak, pripremio sam još jedan primjer: klasu koja koristi nekoliko std::function objekata. Po mojoj zamisli trebalo je i to da pokaže prednost u učinku PPSC-a nad PPZ-om, a ispalo je upravo suprotno! Ali ne navodim ovaj primjer ovdje ne zato što mi se rezultati nisu svidjeli, već zato što nisam imao vremena da shvatim zašto su takvi rezultati dobijeni. Ipak, postoji kod u repozitorijumu (Štampači), testovi - takođe, ako neko želi da shvati, bilo bi mi drago da čujem rezultate istraživanja. Na ovaj primjer planiram se vratiti kasnije, a ako niko prije mene ne objavi ove rezultate, onda ću ih razmotriti u posebnom članku.

Rezultati

Dakle, pogledali smo različite prednosti i nedostatke prosljeđivanja po vrijednosti i prosljeđivanja po referenci na konstantu. Pogledali smo neke primjere i pogledali performanse obje metode u ovim primjerima. Naravno, ovaj članak ne može i nije iscrpan, ali, po mom mišljenju, sadrži dovoljno informacija da se donese nezavisna i informirana odluka o tome koju metodu je najbolje koristiti. Neko bi mogao prigovoriti: "zašto koristiti jednu metodu, počnimo od zadatka!" Iako se slažem s ovom tezom općenito, ne slažem se s njom u ovoj situaciji. Vjerujem da može postojati samo jedan način prenošenja argumenata na jeziku, koji se podrazumevano koristi.

Šta podrazumeva podrazumevano? To znači da kada pišem funkciju, ne razmišljam o tome kako da prenesem argument, samo koristim "default". C++ jezik je prilično složen jezik koji mnogi ljudi izbjegavaju. I po mom mišljenju, složenost nije uzrokovana toliko složenošću jezičkih konstrukcija koje postoje u jeziku (tipični programer ih možda nikada neće susresti), već činjenicom da vas jezik tjera na razmišljanje: jesam li oslobodio memoriju, da li je skupo koristiti ovu funkciju ovdje? i tako dalje.

Mnogi programeri (C, C++ i drugi) su nepovjerljivi i plaše se C++-a koji je počeo da se pojavljuje nakon 2011. godine. Čuo sam dosta kritika da jezik postaje sve složeniji, da na njemu sada mogu pisati samo „gurui“ itd. Lično smatram da to nije tako – naprotiv, komisija posvećuje dosta vremena tome da jezik učini što prijateljskim početnicima i da programeri moraju manje da razmišljaju o karakteristikama jezika. Uostalom, ako ne moramo da se mučimo sa jezikom, onda imamo vremena da razmislimo o zadatku. Ova pojednostavljenja uključuju pametne pokazivače, lambda funkcije i još mnogo toga što se pojavilo u jeziku. Istovremeno, ne poričem činjenicu da sada treba više učiti, ali šta je loše u učenju? Ili se ne dešavaju promjene u drugim popularnim jezicima koje je potrebno naučiti?

Dalje, ne sumnjam da će biti snobova koji mogu odgovoriti: „Ne želite da razmišljate? Onda idi piši u PHP-u.” Takvim ljudima ne želim ni da odgovaram. Navešću samo jedan primer iz realnosti igre: u prvom delu Starcraft-a, kada se u zgradi stvori novi radnik, da bi počeo da vadi minerale (ili gas), morao je biti ručno poslat tamo. Štaviše, svaki paket minerala imao je granicu, po dostizanju koje je povećanje broja radnika bilo beskorisno, a mogli su čak i ometati jedni druge, pogoršavajući proizvodnju. Ovo je promijenjeno u Starcraftu 2: radnici automatski počinju kopati minerale (ili plin), a također pokazuje koliko radnika trenutno rudari i kolika je granica ovog ležišta. Ovo je uvelike pojednostavilo interakciju igrača sa bazom, omogućavajući mu da se fokusira na važnije aspekte igre: izgradnju baze, gomilanje trupa i uništavanje neprijatelja. Čini se da je ovo samo sjajna inovacija, ali ono što je počelo na internetu! Ljudi (ko su oni?) počeli su vrištati da se igra "zajeba" i da su "ubili Starcraft". Očigledno je da su takve poruke mogle doći samo od „čuvara tajnog znanja“ i „adepta visokog APM-a“ koji su voljeli biti u nekom „elitnom“ klubu.

Dakle, vraćajući se na našu temu, što manje treba da razmišljam o tome kako da napišem kod, više vremena imam da razmišljam o rešavanju trenutnog problema. Razmišljanje o tome koju metodu da koristim - PPSC ili PPZ - ne približava me ni za jotu rješavanju problema, pa jednostavno odbijam razmišljati o takvim stvarima i biram jednu opciju: prelazak po referenci na konstantu. Zašto? Jer ne vidim nikakve prednosti za JPP u općim slučajevima, a posebne slučajeve treba razmotriti zasebno.

To je poseban slučaj, samo što sam primijetio da se u nekoj metodi PPSC ispostavlja kao usko grlo, a promjenom prijenosa na PPZ dobićemo značajno povećanje performansi, ne ustručavam se koristiti PPZ. Ali po defaultu ću koristiti PPSC i u regularnim funkcijama i u konstruktorima. I ako je moguće, promovirat ću ovu metodu gdje god je to moguće. Zašto? Jer smatram da je praksa promoviranja PPP-a opaka zbog činjenice da lavovski dio programera nije baš upućen (ili u principu, ili jednostavno još nisu ušli u zamah), i jednostavno slijede savjete. Osim toga, ako postoji nekoliko suprotstavljenih savjeta, biraju onaj koji je jednostavniji, a to dovodi do pesimizma u kodu samo zato što je neko negdje nešto čuo. Oh da, ovaj neko takođe može dati link do Abrahamsovog članka da dokaže da je u pravu. I onda sjedite, čitate kod i razmišljate: da li je činjenica da se parametar ovdje prosljeđuje po vrijednosti zato što je programer koji je ovo napisao došao sa Jave, samo je pročitao puno “pametnih” članaka, ili zaista postoji potreba za tehnička specifikacija?

PPSC je mnogo lakši za čitanje: osoba jasno poznaje „dobru formu“ C++-a i idemo dalje - pogled se ne zadržava. Praksa korišćenja PPSC-a se godinama uči C++ programerima, koji je razlog da se to odustane? Ovo me dovodi do drugog zaključka: ako interfejs metode koristi PPP, onda bi trebalo da postoji i komentar zašto je to tako. U drugim slučajevima mora se primijeniti PPSC. Naravno, postoje tipovi izuzetaka, ali ih ovdje ne spominjem samo zato što se podrazumijevaju: string_view, initializer_list, razni iteratori, itd. Ali ovo su izuzeci, čija se lista može proširiti ovisno o tome koje vrste se koriste u projektu. Ali suština ostaje ista od C++98: po defaultu uvijek koristimo PPCS.

Za std::string najvjerovatnije neće biti razlike na malim stringovima, o tome ćemo kasnije.

Unaprijed se izvinjavam na pretencioznoj napomeni o "postavljanju bodova", ali moramo vas nekako namamiti u članak)) Sa svoje strane, pokušat ću osigurati da sažetak i dalje ispunjava vaša očekivanja.

Ukratko o čemu pričamo

Svi to već znaju, ali na početku ću vas podsjetiti kako se parametri metode mogu proslijediti u 1C. Mogu se proslijediti “po referenci” ili “po vrijednosti”. U prvom slučaju metodi prosljeđujemo istu vrijednost kao na mjestu poziva, au drugom njenu kopiju.

Podrazumevano, u 1C, argumenti se prosleđuju po referenci, a promene parametra unutar metode biće vidljive izvan metode. Ovdje dalje razumijevanje pitanja zavisi od toga šta tačno razumete pod rečju „promena parametra“. Dakle, ovo znači preraspoređivanje i ništa više. Štaviše, dodjela može biti implicitna, na primjer, pozivanje metode platforme koja vraća nešto u izlaznom parametru.

Ali ako ne želimo da naš parametar bude proslijeđen referencom, tada možemo specificirati ključnu riječ prije parametra Značenje

Procedura ByValue(Parametar vrijednosti) Parametar = 2; Parametar EndProcedure = 1; ByValue(parametar); Izvještaj(parametar); // će ispisati 1

Sve radi kako je obećano - promjena (ili bolje rečeno "zamjena") vrijednosti parametra ne mijenja vrijednost izvan metode.

Pa, u čemu je šala?

Zanimljivi trenuci počinju kada kao parametre počnemo prosljeđivati ​​ne primitivne tipove (nizove, brojeve, datume, itd.), već objekte. Ovdje stupaju u igru ​​koncepti kao što su "plitke" i "duboke" kopije objekta, kao i pokazivači (ne u C++ terminima, već kao apstraktne ručke).

Prilikom prosljeđivanja objekta (na primjer, tablice vrijednosti) referencom, mi prosljeđujemo samu vrijednost pokazivača (određeni rukohvat), koji "drži" objekt u memoriji platforme. Kada se proslijedi po vrijednosti, platforma će napraviti kopiju ovog pokazivača.

Drugim riječima, ako, prosljeđujući objekt po referenci, u metodi dodijelimo vrijednost “Niz” parametru, tada ćemo na mjestu poziva primiti niz. Ponovno dodjeljivanje vrijednosti proslijeđene referencom vidljivo je s lokacije poziva.

Procedura ProcessValue(Parametar) Parametar = Novi niz; Tabela završne procedure = Nova tablica vrijednosti; ProcessValue(Tabela); Izvještaj(VrijednostTip(Tabela)); // će ispisati niz

Ako prosledimo objekat po vrednosti, tada na mestu poziva naša Tabela vrednosti neće biti izgubljena.

Sadržaj i stanje objekta

Prilikom prosljeđivanja po vrijednosti, ne kopira se cijeli objekt, već samo njegov pokazivač. Instanca objekta ostaje ista. Nije bitno kako ćete proslijediti objekt, po referenci ili po vrijednosti - brisanjem tablice vrijednosti izbrisat će se i sama tablica. Ovo čišćenje će biti vidljivo svuda, jer... postojao je samo jedan objekat i nije bilo važno kako je tačno prosleđen metodi.

Procedura ProcessValue(Parametar) Parameter.Clear(); Tabela završne procedure = Nova tablica vrijednosti; Table.Add(); ProcessValue(Tabela); Izvještaj(Tabela.Količina()); // će ispisati 0

Prilikom prosljeđivanja objekata metodama, platforma radi sa pokazivačima (uslovnim, a ne direktnim analogama iz C++). Ako se objekt prosljeđuje referencom, tada memorijska ćelija 1C virtualne mašine u kojoj se objekt nalazi može biti prepisana drugim objektom. Ako se objekt prosljeđuje po vrijednosti, tada se pokazivač kopira i prepisivanje objekta ne rezultira prepisivanjem memorijske lokacije originalnim objektom.

U isto vrijeme, svaka promjena stanje objekt (čišćenje, dodavanje svojstava, itd.) mijenja sam objekt i nema nikakve veze s tim kako i gdje je objekt prebačen. Stanje instance objekta se promijenilo; može postojati gomila "referenci" i "vrijednosti" za njega, ali instanca je uvijek ista. Prosljeđivanjem objekta metodi, ne kreiramo kopiju cijelog objekta.

I ovo je uvek tačno, osim...

Interakcija klijent-server

Platforma implementira pozive servera vrlo transparentno. Jednostavno pozivamo metodu, a ispod poklopca platforma serijalizira (pretvara se u niz) sve parametre metode, prosljeđuje ih serveru, a zatim vraća izlazne parametre nazad klijentu, gdje se deserializiraju i žive kao da nikada nisu bili ni na jednom serveru.

Kao što znate, nisu svi objekti platforme serijski. Ovdje raste ograničenje: ne mogu se svi objekti prenijeti na serversku metodu od klijenta. Ako proslijedite objekt koji se ne može serijalizirati, platforma će početi koristiti loše riječi.

  • Eksplicitna izjava o namerama programera. Gledajući potpis metode, možete jasno reći koji su parametri ulazni, a koji izlazni. Ovaj kod je lakši za čitanje i održavanje
  • Da bi promjena parametra “by reference” na serveru bila vidljiva na pozivnoj tački na klijentu, p Sama platforma će nužno vratiti parametre proslijeđene serveru putem linka do klijenta kako bi se osiguralo ponašanje opisano na početku članka. Ako se parametar ne mora vratiti, doći će do prekoračenja prometa. Da bismo optimizirali razmjenu podataka, parametre čije vrijednosti nam nisu potrebne na izlazu treba označiti riječju Vrijednost.

Druga stvar je ovdje vrijedna pažnje. Da bi optimizirala promet, platforma neće vratiti vrijednost parametra klijentu ako je parametar označen riječju Vrijednost. Sve je to odlično, ali dovodi do zanimljivog efekta.

Kao što sam već rekao, kada se objekat prenese na server, dolazi do serijalizacije, tj. izvodi se "duboka" kopija objekta. I ako postoji riječ Značenje objekat neće putovati od servera nazad do klijenta. Dodajemo ove dvije činjenice i dobijemo sljedeće:

&OnServerProcedureByLink(Parametar) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(parametar vrijednosti) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient procedura CheckValue() Lista1= Nove vrijednosti liste; List1.Add("zdravo"); Lista2 = Lista1.Kopiraj(); Lista3 = Lista1.Kopiraj(); // objekt se u potpunosti kopira, // prenosi na server, a zatim se vraća. // brisanje liste je vidljivo na pozivnoj točki ByRef(List1); // objekt se kopira u potpunosti, // prenosi na server. Ne vraća se. // Brisanje liste NIJE VIDLJIVO u trenutku pozivanja ByValue(List2); // kopira se samo pokazivač objekta // brisanje liste je vidljivo na mjestu poziva ByValueClient(List3); Izvještaj(Lista1.Količina()); Izvještaj(Lista2.Količina()); Izvještaj(Lista3.Količina()); Kraj procedure

Sažetak

Ukratko, može se sažeti na sljedeći način:

  • Prenošenje po referenci omogućava vam da "prepišete" objekat sa potpuno drugačijim objektom
  • Prolazak po vrijednosti ne dozvoljava vam da “prepišete” objekt, ali će promjene u unutrašnjem stanju objekta biti vidljive, jer radimo sa istom instancom objekta
  • Prilikom pozivanja servera radi se sa RAZLIČITIM instancama objekta, jer Izvedena je duboka kopija. Ključna riječ Značenjeće spriječiti da se instanca servera vrati nazad u instancu klijenta, a promjena internog stanja objekta na serveru neće dovesti do slične promjene na klijentu.

Nadam se da će vam ova jednostavna lista pravila olakšati rješavanje sporova sa kolegama oko prosljeđivanja parametara "po vrijednosti" i "po referenci"