Paiet garām vērtībai. Parametru nodošana pēc atsauces un vērtības. Noklusējuma iestatījumi

Tātad, lai faktoriāls(n) ir funkcija skaitļa n faktoriāla aprēķināšanai. Tad, ņemot vērā, ka mēs “zinām”, ka faktoriāls 1 ir 1, mēs varam izveidot šādu ķēdi:

Faktoriāls(4)=faktoriāls(3)*4

Faktoriāls(3)=faktoriāls(2)*3

Faktoriāls(2)=faktoriāls(1)*2

Bet, ja mums nebūtu termināla nosacījuma, ka tad, kad n=1, Factorial funkcijai jāatgriež 1, tad šāda teorētiskā ķēde nekad nebūtu beigusies, un tā varētu būt Call Stack Overflow kļūda - zvanu steka pārpilde. Lai saprastu, kas ir zvanu steks un kā tas var pārplūst, apskatīsim mūsu funkcijas rekursīvo ieviešanu:

Funkciju faktoriāls(n: Integer): LongInt;

Ja n=1 Tad

Faktoriāls:=Faktoriāls(n-1)*n;

Beigas;

Kā redzam, lai ķēde darbotos pareizi, pirms katras nākamās funkcijas izsaukšanas ir nepieciešams kaut kur saglabāt visus lokālos mainīgos, lai, ķēdi apgriežot, rezultāts būtu pareizs (aprēķinātā vērtība no faktoriāla n-1 tiek reizināts ar n ). Mūsu gadījumā katru reizi, kad faktoriālā funkcija tiek izsaukta no sevis, ir jāsaglabā visas mainīgā n vērtības. Apgabalu, kurā tiek saglabāti funkcijas lokālie mainīgie, rekursīvi izsaucot sevi, sauc par izsaukuma steku. Protams, šī kaudze nav bezgalīga un var tikt izsmelta, ja rekursīvie izsaukumi tiek konstruēti nepareizi. Mūsu piemēra iterāciju galīgumu garantē fakts, ka, ja n=1, funkcijas izsaukums apstājas.

Parametru nodošana pēc vērtības un atsauces

Līdz šim mēs nevarējām mainīt vērtību apakšprogrammā faktiskais parametrs(t.i., parametrs, kas tiek norādīts, izsaucot apakšprogrammu), un dažos lietojumprogrammas uzdevumos tas būtu ērti. Atcerēsimies Val procedūru, kas maina uzreiz divu tā faktisko parametru vērtību: pirmais ir parametrs, kurā tiks ierakstīta virknes mainīgā konvertētā vērtība, bet otrs ir parametrs Code, kurā tiek norādīts kļūdainā skaitlis. rakstzīme tiek ievietota kļūmes gadījumā tipa pārveidošanas laikā. Tie. joprojām pastāv mehānisms, ar kura palīdzību apakšprogramma var mainīt faktiskos parametrus. Tas ir iespējams, pateicoties dažādiem parametru nodošanas veidiem. Apskatīsim šīs metodes tuvāk.

Programmēšana Paskālā

Parametru nodošana pēc vērtības

Būtībā šādi mēs nodevām visus parametrus mūsu rutīnai. Mehānisms ir šāds: kad ir norādīts faktiskais parametrs, tā vērtība tiek kopēta atmiņas apgabalā, kurā atrodas apakšprogramma, un pēc tam, kad funkcija vai procedūra ir pabeigusi savu darbu, šī zona tiek notīrīta. Aptuveni runājot, kamēr apakšprogramma darbojas, ir divas tās parametru kopijas: viena izsaukšanas programmas tvērumā, bet otrā - funkcijas.

Izmantojot šo parametru nodošanas metodi, apakšprogrammas izsaukšana aizņem vairāk laika, jo papildus pašam izsaukumam ir jākopē visas visu faktisko parametru vērtības. Ja apakšprogrammai tiek nodots liels datu apjoms (piemēram, masīvs ar lielu elementu skaitu), laiks, kas nepieciešams datu kopēšanai lokālajā apgabalā, var būt ievērojams un tas ir jāņem vērā, izstrādājot programmas un atrast vājās vietas savā darbībā.

Izmantojot šo pārsūtīšanas metodi, apakšprogramma nevar mainīt faktiskos parametrus, jo izmaiņas ietekmēs tikai izolētu lokālo apgabalu, kas tiks atbrīvots pēc funkcijas vai procedūras pabeigšanas.

Parametru nodošana pēc atsauces

Ar šo metodi faktisko parametru vērtības netiek kopētas apakšprogrammā, bet tiek pārsūtītas adreses atmiņā (saites uz mainīgajiem), kur tie atrodas. Šajā gadījumā apakšprogramma jau maina vērtības, kas neietilpst vietējā tvērumā, tāpēc visas izmaiņas būs redzamas izsaucējai programmai.

Lai norādītu, ka arguments ir jānodod ar atsauci, pirms tā deklarācijas tiek pievienots atslēgvārds var:

Procedūra getTwoRandom(var n1, n2:Integer; diapazons: Integer);

n1:=nejaušs(diapazons);

n2:=nejaušs(diapazons); beigas ;

var rand1, rand2: vesels skaitlis;

Sāciet getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Beigas.

Šajā piemērā atsauces uz diviem mainīgajiem tiek nodotas procedūrai getTwoRandom kā faktiskie parametri: rand1 un rand2. Trešais faktiskais parametrs (10) tiek nodots pēc vērtības. Procedūra raksta, izmantojot formālos parametrus

Programmēšanas metodes, izmantojot virknes

Laboratorijas darba mērķis : apgūstiet metodes C# valodā, noteikumus darbam ar rakstzīmju datiem un ListBox komponentu. Uzrakstiet programmu darbam ar virknēm.

Metodes

Metode ir klases elements, kas satur programmas kodu. Metodei ir šāda struktūra:

[atribūti] [specifiers] tipa nosaukums ([parametri])

Metodes korpuss;

Atribūti ir īpaši norādījumi kompilatoram par metodes īpašībām. Atribūti tiek izmantoti reti.

Apzīmētāji ir atslēgvārdi, kas kalpo dažādiem mērķiem, piemēram:

· Metodes pieejamības noteikšana citām klasēm:

o Privāts– metode būs pieejama tikai šajā klasē

o aizsargāts– metode būs pieejama arī bērnu klasēm

o publiski– metode būs pieejama jebkurai citai klasei, kas var piekļūt šai klasei

Norāda metodes pieejamību, neveidojot klasi

· Iestatījuma veids

Tips nosaka rezultātu, ko metode atgriež: tas var būt jebkurš C# pieejamais veids, kā arī spēkā esošais atslēgvārds, ja rezultāts nav nepieciešams.

Metodes nosaukums ir identifikators, kas tiks izmantots metodes izsaukšanai. Uz identifikatoru attiecas tādas pašas prasības kā uz mainīgo nosaukumiem: tas var sastāvēt no burtiem, cipariem un pasvītras, bet nevar sākties ar cipariem.

Parametri ir mainīgo lielumu saraksts, kurus var nodot metodei, kad tā tiek izsaukta. Katrs parametrs sastāv no mainīgā veida un nosaukuma. Parametri ir atdalīti ar komatiem.

Metodes pamatteksts ir parasts programmas kods, izņemot to, ka tas nevar saturēt citu metožu definīcijas, klases, nosaukumvietas utt. Ja metodei ir jāatgriež kāds rezultāts, tad atgriešanas atslēgvārdam ir jābūt klāt ar atgriešanās vērtību. . Ja rezultātu atgriešana nav nepieciešama, tad atgriešanas atslēgvārda izmantošana nav nepieciešama, lai gan tas ir atļauts.

Izteiksmes novērtēšanas metodes piemērs:

publiskais dubultā aprēķins (dubultā a, dubultā b, dubultā c)

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

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

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

Metode Pārslodze

C# valoda ļauj izveidot vairākas metodes ar vienādiem nosaukumiem, bet atšķirīgiem parametriem. Kompilators, veidojot programmu, automātiski izvēlēsies piemērotāko metodi. Piemēram, varat uzrakstīt divas atsevišķas metodes skaitļa paaugstināšanai līdz pakāpei: viens algoritms tiks izmantots veseliem skaitļiem, bet otrs tiks izmantots reāliem skaitļiem:

///

/// Aprēķināt X līdz pakāpei Y veseliem skaitļiem

///

privātais int Pow(int X, int Y)

///

/// Reāliem skaitļiem aprēķini X līdz Y pakāpei

///

privāta dubultā Pow (dubultā X, dubultā Y)

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

citādi, ja (Y == 0)

Šo kodu izsauc tādā pašā veidā, atšķirība ir tikai parametros - pirmajā gadījumā kompilators izsauks Pow metodi ar veseliem skaitļiem, bet otrajā - ar reāliem parametriem:

Noklusējuma iestatījumi

C# valoda, sākot ar versiju 4.0 (Visual Studio 2010), ļauj iestatīt noklusējuma vērtības dažiem parametriem, lai, izsaucot metodi, jūs varētu izlaist dažus parametrus. Lai to izdarītu, ieviešot metodi, nepieciešamajiem parametriem ir jāpiešķir vērtība tieši parametru sarakstā:

private void GetData(int Skaitlis, int Neobligāts = 5 )

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

Console.WriteLine("Neobligāti: (0)", neobligāti);

Šajā gadījumā metodi var izsaukt šādi:

GetData(10, 20);

Pirmajā gadījumā izvēles parametrs būs vienāds ar 20, jo tas ir skaidri norādīts, un otrajā gadījumā tas būs vienāds ar 5, jo tas nav skaidri norādīts, un kompilators izmanto noklusējuma vērtību.

Noklusējuma parametrus var iestatīt tikai parametru saraksta labajā pusē, piemēram, kompilators nepieņems šādu metodes parakstu:

private void GetData(int Neobligāts = 5 , int numurs)

Kad parametri metodei tiek nodoti parastajā veidā (bez papildu ref un out atslēgvārdiem), visas metodes parametru izmaiņas neietekmē tās vērtību galvenajā programmā. Pieņemsim, ka mums ir šāda metode:

Private Void Calc(int Number)

Redzams, ka metodes iekšienē tiek mainīts mainīgais Skaitlis, kas tika nodots kā parametrs. Mēģināsim nosaukt metodi:

Console.WriteLine(n);

Ekrānā parādīsies skaitlis 1, tas ir, neskatoties uz mainīgā lieluma izmaiņām Calc metodē, mainīgā vērtība galvenajā programmā nav mainījusies. Tas ir saistīts ar faktu, ka, izsaucot metodi, a kopiju izturēts mainīgais, metode mainās. Kad metode tiek pārtraukta, kopiju vērtība tiek zaudēta. Šo parametra nodošanas metodi sauc iet pa vērtību.

Lai metode mainītu tai nodoto mainīgo, tas ir jānodod ar ref atslēgvārdu — tam jābūt gan metodes parakstā, gan izsaukšanas brīdī:

privāts tukšums Calc (atsauces numurs)

Console.WriteLine(n);

Šajā gadījumā ekrānā parādīsies skaitlis 10: vērtības izmaiņas metodē ietekmēja arī galveno programmu. Šo pārsūtīšanas metodi sauc garāmejot ar atsauci, t.i. Tā vairs nav kopija, kas tiek pārraidīta, bet gan atsauce uz reālu mainīgo atmiņā.

Ja metode izmanto mainīgos ar atsauci tikai vērtību atgriešanai, un tai ir vienalga, kas tajos sākotnēji bija, tad jūs nevarat inicializēt šādus mainīgos, bet gan nodot tos ar out atslēgvārdu. Kompilators saprot, ka mainīgā sākotnējā vērtība nav svarīga un nesūdzas par inicializācijas trūkumu:

privāts tukšums Calc (out int Number)

int n; // Mēs neko nepiešķiram!

virknes datu tips

C# valoda virkņu glabāšanai izmanto virknes tipu. Lai deklarētu (un, kā likums, nekavējoties inicializētu) virknes mainīgo, varat uzrakstīt šādu kodu:

string a = "Teksts";

virkne b = "virknes";

Jūs varat veikt pievienošanas darbību rindām - šajā gadījumā vienas rindas teksts tiks pievienots citas rindas tekstam:

virkne c = a + " " + b; // Rezultāts: virknes teksts

Virknes tips faktiski ir aizstājvārds String klasei, kas ļauj veikt vairākas sarežģītākas darbības ar virknēm. Piemēram, IndexOf metode virknē var meklēt apakšvirkni, un metode Apakšvirkne atgriež noteikta garuma virknes daļu, sākot no noteiktas pozīcijas:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int indekss = a.IndexOf("OP"); // Rezultāts: 14 (skaitot no 0)

virkne b = a.Apakšvirkne(3, 5); // Rezultāts: DEFGH

Ja virknei jāpievieno speciālās rakstzīmes, varat to izdarīt, izmantojot atsoļa secības, kas sākas ar atpakaļvērsto slīpsvītru:

ListBox komponents

Komponents ListBox ir saraksts, kura elementi ir atlasīti, izmantojot tastatūru vai peli. Elementu sarakstu nosaka rekvizīts Preces. Priekšmeti ir elements, kam ir savas īpašības un savas metodes. Metodes Pievienot, NoņemtAt Un Ievietot tiek izmantoti elementu pievienošanai, dzēšanai un ievietošanai.

Objekts Preces saglabā objektus sarakstā. Objekts var būt jebkura klase - klases dati tiek pārveidoti attēlošanai virknes attēlojumā ar ToString metodi. Mūsu gadījumā virknes darbosies kā objekti. Tomēr, tā kā objekts Vienumi saglabā objektus, kas ir nodoti tipa objektam, pirms tā izmantošanas tie ir jāatgriež sākotnējā veidā, mūsu gadījumā — virknē:

string a = (string)listBox1.Items;

Lai noteiktu atlasītā elementa numuru, izmantojiet rekvizītu SelectedIndex.

Kad sāku programmēt C++ valodā un intensīvi studēju grāmatas un rakstus, vienmēr saskāros ar vienu un to pašu padomu: ja kāds objekts ir jānodod funkcijai, kurai funkcijā nevajadzētu mainīties, tad tas vienmēr ir jānodod. atsaucoties uz konstanti(PPSK), izņemot tos gadījumus, kad mums ir jānokārto vai nu primitīvs tips, vai tiem līdzīga struktūra. Jo Vairāk nekā 10 gadus programmējot C++, es ļoti bieži esmu saskāries ar šo padomu (un pats esmu to sniedzis vairāk nekā vienu reizi), tas man jau sen ir “iesūcies” - es automātiski nododu visus argumentus, atsaucoties uz konstanti . Bet laiks iet un ir pagājuši jau 7 gadi, kopš mūsu rīcībā ir C++11 ar tā gājienu semantiku, saistībā ar kuru arvien biežāk dzirdu balsis, kas apšauba veco labo dogmu. Daudzi sāk iebilst, ka pāreja, atsaucoties uz konstanti, ir pagātne un tagad tā ir nepieciešama iet pa vērtību(PPZ). Kas slēpjas aiz šīm sarunām, kā arī kādus secinājumus no tā visa varam izdarīt, vēlos apspriest šajā rakstā.

Grāmatu gudrība

Lai saprastu, kāds noteikums mums jāievēro, iesaku pievērsties grāmatām. Grāmatas ir lielisks informācijas avots, kas mums nav jāpieņem, bet kurā noteikti ir vērts ieklausīties. Un sāksim ar vēsturi, ar pirmsākumiem. Es neuzzināšu, kurš bija pirmais PPSC apoloģēts, vienkārši kā piemēru minēšu grāmatu, kas uz mani personīgi visvairāk ietekmēja PPSC lietošanas jautājumu.

Mayers

Labi, šeit mums ir klase, kurā visi parametri tiek nodoti pēc atsauces, vai ar šo klasi ir kādas problēmas? Diemžēl tā ir, un šī problēma slēpjas virspusē. Mūsu klasē ir 2 funkcionālās entītijas: pirmā ņem vērtību objekta izveides stadijā, bet otrā ļauj mainīt iepriekš iestatīto vērtību. Mums ir divas entītijas, bet četras funkcijas. Tagad iedomājieties, ka mums var būt nevis 2 līdzīgas entītijas, bet gan 3, 5, 6, ko tad? Tad mēs saskarsimies ar nopietnu kodu uzpūšanos. Tāpēc, lai neradītu funkciju masu, tika piedāvāts vispār atteikties no saitēm parametros:

Veidne klases turētājs ( publisks: nepārprotams Turētājs(T vērtība): m_Vērtība(pārvietot(vērtība)) ( ) void setValue(T value) ( m_Value = move(value); ) const T& value() const noexcept ( atgriešanās m_vērtība; ) privāts: T m_Vērtība; );

Pirmā priekšrocība, kas uzreiz iekrīt acīs, ir ievērojami mazāk koda. Tā ir pat mazāk nekā pirmajā versijā, jo tika noņemti const un & (lai gan tie pievienoja kustību). Bet mums vienmēr ir mācīts, ka iet garām ir produktīvāk nekā iet garām! Tā tas bija pirms C++11, un kā ir joprojām, bet tagad, ja mēs paskatīsimies uz šo kodu, mēs redzēsim, ka šeit nav vairāk kopēšanas kā pirmajā versijā, ar nosacījumu, ka T ir pārvietošanās konstruktors. Tie. Pats PPSC bija un būs ātrāks par PPZ, bet kods kaut kā izmanto nodoto atsauci, un bieži vien šis arguments tiek kopēts.

Tomēr tas nav viss stāsts. Atšķirībā no pirmā varianta, kur mums ir tikai kopēšana, šeit mēs pievienojam arī kustību. Bet pārcelšanās ir lēta operācija, vai ne? Par šo tēmu Mayers grāmatā, kuru mēs apsveram, ir arī nodaļa (“29. punkts”), kuras nosaukums ir: “Pieņemsim, ka pārvietošanas darbības nav pieejamas, nav lētas un netiek izmantotas.” Galvenajai domai jābūt skaidrai no virsraksta, bet, ja vēlaties sīkāku informāciju, tad noteikti izlasiet to - es pie tā nekavēšos.

Šeit būtu pareizi veikt pirmās un pēdējās metodes pilnīgu salīdzinošu analīzi, taču es negribētu atkāpties no grāmatas, tāpēc mēs atliksim analīzi citām sadaļām, un šeit mēs turpināsim izskatīt Skota argumentus. Tātad, ja neskaita faktu, ka trešā iespēja acīmredzami ir īsāka par otro, ko Skots uzskata par PPZ priekšrocību salīdzinājumā ar PPSC mūsdienu kodā?

Viņš to saskata tajā, ka rvērtības nodošanas gadījumā, t.i. daži sauc šādi: Holder holder(string("es")); , opcija ar PPSC nodrošinās mums kopēšanu, un opcija ar PPZ dos mums kustību. No otras puses, ja pārskaitījums ir šāds: Holder holder(someLvalue); , tad PPZ noteikti zaudē tāpēc, ka veiks gan kopēšanu, gan pārvietošanu, savukārt versijā ar PPSC kopēšana būs tikai viena. Tie. izrādās, ka PPZ, ja ņemam vērā tīri efektivitāti, ir kaut kāds kompromiss starp koda daudzumu un “pilnu” (caur && ) kustību semantikas atbalstu.

Tāpēc Skots tik rūpīgi formulēja savus padomus un tik rūpīgi tos popularizē. Man pat šķita, ka viņš to aktualizēja negribīgi, it kā pakļauts spiedienam: viņš nevarēja neievietot grāmatā diskusijas par šo tēmu, jo... tas tika apspriests diezgan plaši, un Skots vienmēr bija kolektīvās pieredzes vācējs. Turklāt viņš sniedz ļoti maz argumentu PPZ aizstāvībai, bet viņš sniedz daudz tādu, kas apšauba šo “tehniku”. Mēs apskatīsim viņa argumentus pret turpmākajās sadaļās, bet šeit mēs īsi atkārtosim argumentu, ko Skots izteica, aizstāvot PPP (garīgi piebilstot "ja objekts atbalsta kustību un tas ir lēts"): ļauj izvairīties no kopēšanas, nododot rvalue izteiksmi kā funkcijas argumentu. Bet pietiekami mocīt Meyers grāmatu, pāriesim pie citas grāmatas.

Starp citu, ja kāds ir lasījis grāmatu un ir pārsteigts, ka es šeit neiekļauju opciju, ko Mayers sauca par universālajām atsaucēm - tagad pazīstamas kā pārsūtīšanas atsauces -, tad tas ir viegli izskaidrojams. Es apsveru tikai PPZ un PPSC, jo... Es uzskatu, ka ir slikta forma ieviest veidņu funkcijas metodēm, kas nav veidnes, tikai tādēļ, lai atbalstītu nodošanu ar atsauci uz abiem veidiem (rvalue/lvalue). Nemaz nerunājot par to, ka kods izrādās atšķirīgs (vairs nav nemainīgs) un rada citas problēmas.

Josattis un kompānija

Pēdējā grāmata, kuru apskatīsim, ir “C++ Templates”, kas ir arī jaunākā no visām šajā rakstā minētajām grāmatām. Tas izdots 2017. gada beigās (un grāmatas iekšpusē norādīts 2018. gads). Atšķirībā no citām grāmatām, šī ir pilnībā veltīta modeļiem, nevis padomiem (kā Mayers) vai C++ vispār, piemēram, Stroustrup. Tāpēc šeit tiek apskatīti plusi/pret no veidņu rakstīšanas viedokļa.

Visa 7. nodaļa ir veltīta šai tēmai, kurai ir daiļrunīgs nosaukums “Pēc vērtības vai atsauces?”. Šajā nodaļā autori diezgan īsi, bet kodolīgi apraksta visas pārraides metodes ar visiem to plusiem un mīnusiem. Efektivitātes analīze šeit praktiski netiek sniegta, un tiek uzskatīts, ka PPSC būs ātrāks par PPZ. Bet ar visu šo, nodaļas beigās autori iesaka veidņu funkcijām izmantot noklusējuma PPP. Kāpēc? Tā kā, izmantojot saiti, veidņu parametri tiek parādīti pilnībā, un bez saites tie tiek “sabrukuši”, kas labvēlīgi ietekmē masīvu un virkņu literāļu apstrādi. Autori uzskata, ka, ja kādam PPP veidam tas izrādās neefektīvs, tad vienmēr var izmantot std::ref un std::cref . Šis ir daži padomi, godīgi sakot, vai esat redzējuši daudzus cilvēkus, kuri vēlas izmantot iepriekš minētās funkcijas?

Ko viņi iesaka saistībā ar PPSC? Viņi iesaka izmantot PPSC, ja veiktspēja ir kritiska vai ir citi smags iemeslus neizmantot PPP. Protams, mēs šeit runājam tikai par standarta kodu, taču šis padoms ir tiešā pretrunā visam, ko programmētāji ir mācījuši desmit gadus. Tas nav tikai padoms apsvērt PPP kā alternatīvu – nē, tas ir ieteikums padarīt PPSC par alternatīvu.

Ar to mūsu grāmatu tūre noslēdzas, jo... Es nezinu nevienu citu grāmatu, ar kuru mums vajadzētu konsultēties par šo jautājumu. Pāriesim uz citu mediju telpu.

Tīkla gudrība

Jo Mēs dzīvojam interneta laikmetā, tad nevajadzētu paļauties tikai uz grāmatu gudrību. Turklāt daudzi autori, kas agrāk rakstīja grāmatas, tagad vienkārši raksta emuārus un ir pametuši grāmatas. Viens no šiem autoriem ir Herbs Saters, kurš 2013. gada maijā publicēja rakstu savā emuārā “GotW #4 Solution: Class Mechanics”, kas, lai arī nav pilnībā veltīts mūsu aplūkotajai problēmai, tomēr tai pieskaras.

Tātad raksta sākotnējā versijā Saters vienkārši atkārtoja veco gudrību: “nodot parametrus, atsaucoties uz konstanti”, bet mēs vairs neredzēsim šo raksta versiju, jo Rakstā ir pretējs padoms: “ Ja parametrs joprojām tiks kopēts, pēc tam nododiet to pēc vērtības. Atkal bēdīgi slavenais “ja”. Kāpēc Saters mainīja rakstu, un kā es par to uzzināju? No komentāriem. Izlasi viņa raksta komentārus, starp citu, tie ir interesantāki un noderīgāki par pašu rakstu. Tiesa, pēc raksta tapšanas Saters beidzot mainīja savu viedokli un šādus padomus viņš vairs nesniedz. Viedokļu izmaiņas var atrast viņa runā CppCon 2014. gadā: “Atgriezties pie pamatiem! Mūsdienu C++ stila pamati”. Noteikti apskatiet, mēs pāriesim uz nākamo interneta saiti.

Un nākamais mums ir galvenais 21. gadsimta programmēšanas resurss: StackOverflow. Vai drīzāk atbilde, pozitīvo reakciju skaits pārsniedz 1700 šī raksta tapšanas laikā. Jautājums ir: kas ir kopēšanas un apmaiņas idioma? , un, kā jau nosaukumā vajadzētu liecināt, ne gluži par tēmu, kuru mēs aplūkojam. Taču savā atbildē uz šo jautājumu autors pieskaras arī tēmai, kas mūs interesē. Viņš arī iesaka izmantot PPZ, “ja arguments tik un tā tiks kopēts” (Dievs ir pienācis laiks ieviest saīsinājumu arī šim vārdam). Un vispār šis padoms šķiet diezgan atbilstošs viņa atbildes un tur apspriestā operatora ietvaros, taču autors uzņemas brīvību sniegt šādu padomu plašāk, un ne tikai šajā konkrētajā gadījumā. Turklāt viņš sniedzas tālāk par visiem iepriekš apspriestajiem padomiem un aicina to darīt pat C++03 kodā! Kas autoru pamudināja izdarīt šādus secinājumus?

Acīmredzot atbildes autors galveno iedvesmu smēlies no cita grāmatu autora un Boost.MPL nepilna laika izstrādātāja - Deiva Abrahamsa raksta. Raksta nosaukums ir “Gribi ātrumu? Paiet garām vērtībai.” , un tas tika publicēts tālajā 2009. gada augustā, t.i. 2 gadus pirms C++11 pieņemšanas un pārvietošanās semantikas ieviešanas. Tāpat kā iepriekšējos gadījumos, iesaku lasītājam rakstu izlasīt pašam, bet es minēšu galvenos argumentus (patiesībā ir tikai viens arguments), ko Deivs sniedz par labu PPZ: jums ir jāizmanto PPZ. , jo ar to labi darbojas optimizācija “izlaist kopiju” ( copy elision), kuras PPSC nav. Palasot komentārus pie raksta, var redzēt, ka viņa virzītie padomi nav universāli, ko apstiprina pats autors, atbildot uz komentētāju kritiku. Tomēr rakstā ir ietverts nepārprotams padoms (vadlīnijas) izmantot PPP, ja arguments tik un tā tiks kopēts. Starp citu, ja kādam ir interese, var izlasīt rakstu “Gribi ātrumu? Nepalaidiet (vienmēr) garām vērtību. . Kā jau nosaukumā vajadzētu norādīt, šis raksts ir atbilde uz Deiva rakstu, tāpēc, ja lasi pirmo, noteikti izlasi arī šo!

Diemžēl (dažiem par laimi) šādi raksti un (vēl jo vairāk) populāras atbildes populārās vietnēs izraisa masveida apšaubāmu paņēmienu izmantošanu (triviāls piemērs), jo tas prasa mazāk rakstīšanas, un vecā dogma vairs nav nesatricināma - Jūs vienmēr varat atsaukties uz “šo populāro padomu”, ja esat piespiests pie sienas. Tagad es iesaku jums iepazīties ar dažādiem resursiem, kas mums piedāvā ieteikumus koda rakstīšanai.

Jo Tā kā dažādi standarti un ieteikumi tagad ir publicēti arī tiešsaistē, es nolēmu klasificēt šo sadaļu kā “tīkla gudrība”. Tātad, šeit es gribētu runāt par diviem avotiem, kuru mērķis ir padarīt C++ programmētāju kodu labāku, sniedzot viņiem padomus (vadlīnijas), kā rakstīt tieši šo kodu.

Pirmais noteikumu kopums, ko es vēlos apsvērt, bija pēdējais piliens, kas piespieda mani pieņemt šo rakstu. Šis komplekts ir daļa no clang-tidy utilīta, un ārpus tā nepastāv. Tāpat kā viss, kas saistīts ar zvanīšanu, šī utilīta ir ļoti populāra un jau ir saņēmusi integrāciju ar CLion un Resharper C++ (tieši tā es ar to saskāros). Tātad, clang-tydy ietver vērtību modernizācijas kārtulu, kas darbojas konstruktoros, kuri pieņem argumentus, izmantojot PPSC. Šis noteikums liek domāt, ka PPSC jāaizstāj ar PPZ. Turklāt raksta tapšanas laikā šī noteikuma aprakstā ir piezīme, ka šis noteikums Uz redzēšanos darbojas tikai konstruktoriem, bet viņi (kas viņi ir?) labprāt pieņems palīdzību no tiem, kas šo noteikumu attiecina arī uz citām entītijām. Tur aprakstā ir saite uz Deiva rakstu - ir skaidrs, no kurienes nāk kājas.

Visbeidzot, lai pabeigtu šo citu cilvēku gudrības un autoritatīvo viedokļu apskatu, iesaku apskatīt oficiālās vadlīnijas C++ koda rakstīšanai: C++ Core Guidelines, kuru galvenie redaktori ir Herbs Saters un Bjarne Stroustrup (nav slikti, vai ne?). Tātad šajos ieteikumos ir ietverts šāds noteikums: “Par parametriem “in” nododiet lēti kopētus tipus pēc vērtības un citus pēc atsauces uz const”, kas pilnībā atkārto veco gudrību: PPSK visur un PPP maziem objektiem. Šajā ieteikumā ir norādītas vairākas alternatīvas, kas jāapsver. ja argumentu nodošanai nepieciešama optimizācija. Bet PPZ nav iekļauts alternatīvu sarakstā!

Tā kā man nav citu uzmanības vērtu avotu, es ierosinu pāriet uz abu pārraides metožu tiešu analīzi.

Analīze

Viss iepriekšējais teksts ir uzrakstīts man nedaudz neparastā veidā: es izklāstu citu cilvēku viedokli un pat cenšos neizteikt savu (zinu, ka tas izrādās slikti). Lielā mērā tāpēc, ka citu viedokļi un mans mērķis bija sniegt īsu to pārskatu, es atliku atsevišķu argumentu detalizētu izskatīšanu, ko atradu pie citiem autoriem. Šajā sadaļā es neatsaucīšos uz autoritātēm un nesniegšu viedokļus, šeit aplūkosim dažas PPSC un PPZ objektīvās priekšrocības un trūkumus, kas tiks piesātināti ar manu subjektīvo uztveri. Protams, daļa no iepriekš apspriestā tiks atkārtota, bet diemžēl tāda ir šī raksta struktūra.

Vai PPP ir priekšrocības?

Tāpēc, pirms apsvērt argumentus par un pret, ierosinu apskatīt, ko un kādos gadījumos mums dod priekšrocības, ko dod garāmejot. Pieņemsim, ka mums ir šāda klase:

Class CopyMover ( publisks: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValueer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuer(Accounter byValueer) m_ByValuer over = byVal uerAndNotMover; ) spēkā neesošs setRvaluer (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Lai gan šī raksta vajadzībām mūs interesē tikai pirmās divas funkcijas, esmu iekļāvis četras iespējas, lai tās izmantotu kā kontrastu.

Klase Accounter ir vienkārša klase, kurā tiek uzskaitīts, cik reižu tā ir kopēta/pārvietota. Un CopyMover klasē esam ieviesuši funkcijas, kas ļauj mums apsvērt šādas iespējas:

    pārvietojas pieņemts arguments.

    Paiet garām vērtībai, kam seko kopēšana pieņemts arguments.

Tagad, ja mēs katrai no šīm funkcijām nododam vērtību, piemēram, šādi:

Grāmatvedis pēc Refer; Grāmatvedis pēc Valuer; grāmatvedis ar ValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

tad mēs iegūstam šādus rezultātus:

Acīmredzamais uzvarētājs ir PPSC, jo... dod tikai vienu eksemplāru, savukārt PPZ dod vienu eksemplāru un vienu gājienu.

Tagad mēģināsim nodot rvērtību:

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

Mēs iegūstam sekojošo:

Šeit nav skaidrs uzvarētājs, jo... gan PPZ, gan PPSK ir pa vienai operācijai, bet sakarā ar to, ka PPZ izmanto kustību, bet PPSK izmanto kopēšanu, uzvaru varam atdot PPZ.

Taču mūsu eksperimenti ar to nebeidzas; pievienosim šādas funkcijas, lai simulētu netiešo zvanu (ar sekojošu argumentu nodošanu):

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

Mēs tos izmantosim tieši tāpat kā bez tiem, tāpēc kodu neatkārtošu (ja nepieciešams, meklējiet repozitorijā). Tātad lvvalu rezultāti būtu šādi:

Ņemiet vērā, ka PPSC palielina atstarpi ar PPZ, paliekot ar vienu eksemplāru, savukārt PPZ jau ir 3 darbības (vēl viena kustība)!

Tagad mēs nododam rvērtību un iegūstam šādus rezultātus:

Tagad PPZ ir 2 kustības, un PPSC joprojām ir viens eksemplārs. Vai tagad ir iespējams izvirzīt PPZ kā uzvarētāju? Nē, jo ja vienai kustībai jābūt vismaz ne sliktākai par vienu eksemplāru, mēs nevaram teikt to pašu par 2 gājieniem. Tāpēc šajā piemērā nebūs uzvarētāja.

Viņi var man iebilst: “Autor, jums ir neobjektīvs viedoklis un jūs velkat to, kas jums ir izdevīgs. Pat 2 gājieni būs lētāki nekā kopēšana!” Es nevaru piekrist šim apgalvojumam Visā visumā, jo Tas, cik daudz ātrāka ir pārvietošana nekā kopēšana, ir atkarīgs no konkrētās klases, bet par "lētu" pārvietošanu apskatīsim atsevišķā sadaļā.

Šeit mēs pieskārāmies interesantai lietai: pievienojām vienu netiešo zvanu, un PPP pievienoja tieši vienu darbību “svarā”. Domāju, ka nav nepieciešams diploms no MSTU, lai saprastu, jo vairāk mums būs netiešo zvanu, jo vairāk operāciju tiks veikts, izmantojot PPZ, savukārt PPSC numurs paliks nemainīgs.

Viss, kas tika apspriests iepriekš, visticamāk, nevienam nebūs atklājums, iespējams, mēs pat nebūtu veikuši eksperimentus - visiem šiem skaitļiem vajadzētu būt acīmredzamiem lielākajai daļai C++ programmētāju no pirmā acu uzmetiena. Tiesa, viens punkts vēl ir pelnījis precizējumu: kāpēc rvalue gadījumā PZ nav kopijas (vai cita gājiena), bet tikai viens gājiens.

Mēs apskatījām atšķirību pārraidē starp PPZ un PPSC, tieši novērojot kopiju un kustību skaitu. Lai gan ir acīmredzams, ka PPZ priekšrocība pār PPSC pat šādos vienkāršos piemēros ir, maigi izsakoties Nav Acīmredzot es joprojām, nedaudz pretenciozi, izdaru šādu secinājumu: ja mēs joprojām kopēsim funkcijas argumentu, tad ir jēga apsvērt argumenta nodošanu funkcijai pēc vērtības. Kāpēc es izdarīju šādu secinājumu? Lai vienmērīgi pārietu uz nākamo sadaļu.

Ja mēs kopējam...

Tātad, mēs nonākam pie sakāmvārda “ja”. Lielākā daļa argumentu, ar kuriem mēs saskārāmies, neaicināja PPSC vietā vispārēji ieviest PPP; viņi aicināja to darīt tikai “ja arguments tik un tā tiek kopēts”. Ir pienācis laiks noskaidrot, kas šajā argumentā ir nepareizi.

Es vēlos sākt ar nelielu aprakstu par to, kā es rakstīju kodu. Pēdējā laikā mans kodēšanas process arvien vairāk līdzinās TDD, t.i. jebkuras klases metodes rakstīšana sākas ar testa rakstīšanu, kurā šī metode parādās. Attiecīgi, sākot rakstīt testu un veidojot metodi pēc testa rakstīšanas, es joprojām nezinu, vai es kopēšu argumentu. Protams, ne visas funkcijas tiek izveidotas šādā veidā, bieži vien pat testa rakstīšanas procesā jūs precīzi zināt, kāda veida ieviešana būs. Bet tas ne vienmēr notiek!

Kāds man varētu iebilst, ka nav svarīgi, kā metode sākotnēji tika uzrakstīta, mēs varam mainīt to, kā mēs nododam argumentu, kad metode ir ieguvusi formu un mums ir pilnīgi skaidrs, kas tur notiek (t.i., vai mums ir kopēšana vai nē). Es tam daļēji piekrītu - tiešām, jūs varat darīt to šādā veidā, bet tas mūs iesaista kaut kādā dīvainā spēlē, kurā mums ir jāmaina saskarnes tikai tāpēc, ka ir mainījusies ieviešana. Kas mūs noved pie nākamās dilemmas.

Izrādās, ka mēs modificējam (vai pat plānojam) saskarni, pamatojoties uz to, kā tā tiks ieviesta. Es neuzskatu sevi par OOP un citu programmatūras arhitektūras teorētisko aprēķinu ekspertu, taču šādas darbības nepārprotami ir pretrunā ar pamatnoteikumiem, kad ieviešanai nevajadzētu ietekmēt saskarni. Protams, atsevišķas ieviešanas detaļas (neatkarīgi no tā, vai tās ir valodas vai mērķa platformas funkcijas) tā vai citādi noplūst caur saskarni, taču jums vajadzētu mēģināt samazināt, nevis palielināt šādu lietu skaitu.

Nu, Dievs viņu svētī, ejam pa šo ceļu un joprojām mainīsim saskarnes atkarībā no tā, ko mēs darām ieviešanā argumenta kopēšanas ziņā. Pieņemsim, ka mēs rakstījām šo metodi:

Nederīgs setName(nosaukums) ( m_Name = pārvietot(nosaukums); )

un veica izmaiņas krātuvē. Laikam ejot, mūsu programmatūras produkts ieguva jaunu funkcionalitāti, tika integrēti jauni ietvari, un radās uzdevums informēt ārpasauli par izmaiņām mūsu klasē. Tie. Mēs savai metodei pievienosim paziņojumu mehānismu, lai tas būtu kaut kas līdzīgs Qt signāliem:

Nederīgs setName(nosaukums) ( m_Nosaukums = pārvietot(nosaukums); emitēt nosaukumuChanged(m_Name); )

Vai ir radusies problēma ar šo kodu? Ēst. Katram zvanam setName mēs nosūtām signālu, tāpēc signāls tiks nosūtīts pat tad, kad nozīmē m_Name nav mainījies. Papildus veiktspējas problēmām šī situācija var izraisīt bezgalīgu cilpu, jo kods, kas saņem iepriekš minēto paziņojumu, kaut kādā veidā izsauc setName. Lai izvairītos no visām šīm problēmām, šādas metodes visbiežāk izskatās šādi:

Nederīgs setName(nosaukums) ( if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); )

Atbrīvojāmies no iepriekš aprakstītajām problēmām, bet tagad mūsu “ja mēs tik un tā kopēsim...” noteikums ir cietis neveiksmi - vairs nenotiek argumenta beznosacījumu kopēšana, tagad kopējam tikai tad, ja tas mainās! Tātad, kas mums tagad jādara? Vai mainīt saskarni? Labi, mainīsim klases saskarni šī labojuma dēļ. Kā būtu, ja mūsu klase šo metodi mantotu no kādas abstraktas saskarnes? Mainīsim to arī tur! Vai ir daudz izmaiņu, jo ir mainījusies ieviešana?

Atkal viņi var iebilst pret mani, viņi saka, autor, kāpēc jūs mēģināt ietaupīt naudu uz spēlēm, ja šis nosacījums tur nostrādās? Jā, lielākā daļa zvanu būs nepatiesi! Vai ir kāda pārliecība par to? Kur? Un, ja es nolēmu ietaupīt uz mačiem, vai tas, ka mēs izmantojām PPZ, nebija tieši šāda ietaupījuma sekas? Es tikai turpinu "partijas līniju", kas iestājas par efektivitāti.

Konstruktori

Īsi apskatīsim konstruktorus, jo īpaši tāpēc, ka tiem ir īpašs noteikums klangāšanā, kas vēl nedarbojas citām metodēm/funkcijām. Pieņemsim, ka mums ir šāda klase:

Klase JustClass ( publiska: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Acīmredzot parametrs tiek kopēts, un clang-tidy mums pateiks, ka būtu laba ideja pārrakstīt konstruktoru uz šo:

JustClass(virkne justString): m_JustString(pārvietot(justString)) ( )

Un, atklāti sakot, man šeit ir grūti strīdēties - galu galā mēs patiešām vienmēr kopējam. Un visbiežāk, kad mēs kaut ko izlaižam caur konstruktoru, mēs to kopējam. Bet biežāk tas nenozīmē vienmēr. Šeit ir vēl viens piemērs:

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

Šeit mēs ne vienmēr kopējam, bet tikai tad, kad datumi ir pareizi uzrādīti. Protams, vairumā gadījumu tas tā būs. Bet ne vienmēr.

Var dot citu piemēru, bet šoreiz bez koda. Iedomājieties, ka jums ir klase, kas pieņem lielu objektu. Klase pastāv jau ilgu laiku, un tagad ir pienācis laiks atjaunināt tās ieviešanu. Mēs saprotam, ka mums ir vajadzīga ne vairāk kā puse no lielas iekārtas (kas gadu gaitā ir pieaugusi), un varbūt pat mazāk. Vai mēs varam kaut ko darīt lietas labā? Nē, mēs nevarēsim neko darīt, jo kopija joprojām tiks izveidota. Bet, ja mēs izmantotu PPSC, mēs vienkārši mainītu to, ko darām iekšā dizainers. Un tas ir galvenais: izmantojot PPSC, mēs kontrolējam, kas un kad notiek mūsu funkcijas (konstruktora) īstenošanā, bet, ja mēs izmantojam PPZ, mēs zaudējam jebkādu kontroli pār kopēšanu.

Ko jūs varat paņemt no šīs sadaļas? Tas, ka arguments “ja mēs tik un tā kopētu...” ir ļoti strīdīgs, jo Mēs ne vienmēr zinām, ko kopēsim, un pat tad, kad zinām, mēs ļoti bieži neesam pārliecināti, ka tas turpināsies arī nākotnē.

Pārcelšanās ir lēta

Jau no brīža, kad parādījās kustības semantika, tā sāka nopietni ietekmēt mūsdienu C++ koda rakstīšanas veidu, un laika gaitā šī ietekme ir tikai pastiprinājusies: nav brīnums, jo kustība ir tik lēts salīdzinot ar kopēšanu. Bet vai tā ir? Vai tā ir taisnība, ka kustība ir Vienmēr lēta operācija? To mēs centīsimies noskaidrot šajā sadaļā.

Binārs liels objekts

Sāksim ar triviālu piemēru, pieņemsim, ka mums ir šāda klase:

Struct Blob ( std::masīvs dati; );

Parasta lāse(BDO, angļu BLOB), ko var izmantot dažādās situācijās. Apskatīsim, cik mums izmaksās nokārtošana pēc atsauces un vērtības. Mūsu BDO tiks izmantots šādi:

Void Storage::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) tukša krātuve::setBlobByVal(Blob blob) ( m_Blob = pārvietot(blob); )

Un mēs šīs funkcijas sauksim šādi:

Const Blob blob(); uzglabāšana; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Citiem piemēriem kods būs identisks šim, tikai ar dažādiem nosaukumiem un veidiem, tāpēc pārējiem piemēriem es to nesniegšu - viss ir repozitorijā.

Pirms pārejam uz mērījumiem, mēģināsim paredzēt rezultātu. Tātad mums ir 4 KB std::masīvs, ko vēlamies saglabāt krātuves klases objektā. Kā noskaidrojām iepriekš, PPSC mums būs viens eksemplārs, savukārt PPZ mums būs viens eksemplārs un viens gājiens. Pamatojoties uz to, ka masīvu nav iespējams pārvietot, PPZ būs 2 kopijas, salīdzinot ar vienu PPSC. Tie. varam sagaidīt PPSC divkāršu pārsvaru sniegumā.

Tagad apskatīsim testa rezultātus:

Šis un visi turpmākie testi tika veikti tajā pašā mašīnā, izmantojot MSVS 2017 (15.7.2) un karogu /O2.

Prakse sakrita ar pieņēmumu - iet pa vērtību ir 2 reizes dārgāk, jo masīvam pārvietošana ir pilnīgi līdzvērtīga kopēšanai.

Līnija

Apskatīsim citu piemēru — parastu std::string . Ko mēs varam sagaidīt? Mēs zinām (es to apspriedu rakstā), ka mūsdienu implementācijās izšķir divu veidu virknes: īsu (apmēram 16 rakstzīmes) un garu (tās, kas ir garākas nekā īsas). Īsajiem tiek izmantots iekšējais buferis, kas ir parasts C-masīvs char , bet garie jau tiks novietoti uz kaudzes. Īsās rindas mūs neinteresē, jo... rezultāts būs tāds pats kā ar BDO, tāpēc pievērsīsimies garām rindām.

Tātad, ja ir gara virkne, ir acīmredzams, ka tās pārvietošanai vajadzētu būt diezgan lētai (tikai pārvietojiet rādītāju), tāpēc varat paļauties uz to, ka virknes pārvietošanai nevajadzētu ietekmēt rezultātus un PPZ ir jādod rezultāts ne sliktāks par PPSC. Pārbaudīsim to praksē un iegūsim šādus rezultātus:

Mēs turpināsim izskaidrot šo "parādību". Tātad, kas notiek, kad mēs kopējam esošu virkni jau esošā virknē? Apskatīsim triviālu piemēru:

Virkne vispirms(64, "C"); virkne second(64, "N"); //... otrais = pirmais;

Mums ir divas 64 rakstzīmju virknes, tāpēc iekšējais buferis nav pietiekams, veidojot tās, kā rezultātā kaudzītē tiek piešķirtas abas virknes. Tagad mēs kopējam pirmo uz otro. Jo mūsu rindu izmēri ir vienādi, acīmredzot otrajā vietā ir pietiekami daudz vietas, lai ievietotu visus datus no pirmā, tāpēc otrais = pirmais; būs banāls memcijs, nekas vairāk. Bet, ja mēs skatāmies uz nedaudz pārveidotu piemēru:

Virkne vispirms(64, "C"); virkne otrais = pirmais;

tad vairs nebūs zvans operatoram= , bet gan tiks izsaukts kopiju konstruktors. Jo Tā kā mums ir darīšana ar konstruktoru, tajā nav esošas atmiņas. Vispirms tas ir jāizvēlas un tikai pēc tam vispirms jākopē. Tie. tas ir atmiņas piešķiršana un pēc tam memcpy. Kā jūs un es zinām, atmiņas piešķiršana globālajā kaudzītē parasti ir dārga darbība, tāpēc kopēšana no otrā piemēra būs dārgāka nekā kopēšana no pirmā. Dārgāka par kaudzes atmiņas piešķiršanu.

Kāds tam sakars ar mūsu tēmu? Vistiešākā, jo pirmajā piemērā ir precīzi parādīts, kas notiek ar PPSC, bet otrais parāda, kas notiek ar PPZ: PPZ vienmēr tiek izveidota jauna rinda, savukārt PPSC tiek atkārtoti izmantota esošā. Jūs jau redzējāt atšķirību izpildes laikā, tāpēc šeit nav ko piebilst.

Šeit mēs atkal saskaramies ar faktu, ka, izmantojot PPP, mēs strādājam ārpus konteksta un tāpēc nevaram izmantot visas priekšrocības, ko tas var sniegt. Un, ja agrāk mēs spriedām par teorētiskām nākotnes izmaiņām, tad šeit mēs novērojam ļoti konkrētu neveiksmi produktivitātē.

Protams, kāds varētu man iebilst, ka virkne atšķiras, un lielākā daļa veidu tā nedarbojas. Uz ko es varu atbildēt šādi: viss, kas aprakstīts iepriekš, būs patiess jebkuram konteineram, kas elementu pakotnei nekavējoties piešķir atmiņu kaudzē. Turklāt, kurš zina, kādas citas kontekstjutīgas optimizācijas tiek izmantotas citos veidos?

Kas jums būtu jāatņem no šīs sadaļas? Tas, ka pat tad, ja pārvietošana ir patiešām lēta, nenozīmē, ka kopēšanas aizstāšana ar kopēšanu+pārvietošana vienmēr dos veiktspējas ziņā salīdzināmu rezultātu.

Sarežģīts tips

Visbeidzot, apskatīsim veidu, kas sastāvēs no vairākiem objektiem. Lai tā būtu Personas klase, kas sastāv no personai raksturīgiem datiem. Parasti tas ir jūsu vārds, uzvārds, pasta indekss utt. To visu varat attēlot kā virknes un pieņemt, ka virknes, kuras ievietojat Personu klases laukos, visticamāk, būs īsas. Lai gan uzskatu, ka dzīvē visnoderīgāk noderēs īso stīgu mērīšana, tomēr apskatīsim dažāda izmēra virknes, lai sniegtu pilnīgāku priekšstatu.

Es izmantošu arī Personu ar 10 laukiem, bet šim es neveidošu 10 laukus tieši klases pamattekstā. Personas realizācija slēpj konteineru savā dziļumā – tas ļauj ērtāk mainīt testa parametrus, praktiski neatkāpjoties no tā, kā tas darbotos, ja Persona būtu īsta klase. Tomēr ieviešana ir pieejama, un jūs vienmēr varat pārbaudīt kodu un pateikt man, ja es kaut ko izdarīju nepareizi.

Tātad, ejam: Persona ar 10 laukiem tipa virknes , ko mēs, izmantojot PPSC un PPZ, pārsūtām uz krātuvi:

Kā redzat, mums ir milzīga veiktspējas atšķirība, kas lasītājiem pēc iepriekšējām sadaļām nevajadzētu būt pārsteigumam. Es arī uzskatu, ka Personu klase ir pietiekami "īsta", lai šādi rezultāti netiktu noraidīti kā abstrakti.

Starp citu, gatavojot šo rakstu, es sagatavoju vēl vienu piemēru: klase, kas izmanto vairākus std::function objektus. Pēc manas idejas arī bija jāparāda pārsvars PPSC sniegumā pār PPZ, bet sanāca tieši otrādi! Bet es nesniedzu šo piemēru nevis tāpēc, ka man nepatika rezultāti, bet gan tāpēc, ka man nebija laika izdomāt, kāpēc šādi rezultāti tika iegūti. Neskatoties uz to, repozitorijā ir kods (Printeri), testi - arī, ja kāds vēlas to izdomāt, es priecātos dzirdēt par pētījuma rezultātiem. Es plānoju atgriezties pie šī piemēra vēlāk, un, ja neviens šos rezultātus nepublicēs pirms manis, es tos apskatīšu atsevišķā rakstā.

Rezultāti

Tāpēc mēs esam aplūkojuši dažādus plusus un mīnusus, kas saistīti ar vērtību nodošanu un nodošanu, atsaucoties uz konstanti. Mēs apskatījām dažus piemērus un aplūkojām abu metožu veiktspēju šajos piemēros. Protams, šis raksts nevar un nav izsmeļošs, taču, manuprāt, tajā ir pietiekami daudz informācijas, lai pieņemtu neatkarīgu un apzinātu lēmumu par to, kuru metodi vislabāk izmantot. Kāds var iebilst: "kāpēc izmantot vienu metodi, sāksim no uzdevuma!" Lai gan es piekrītu šai tēzei kopumā, es tai nepiekrītu šajā situācijā. Es uzskatu, ka var būt tikai viens veids, kā nodot argumentus valodā, kas tiek izmantots pēc noklusējuma.

Ko nozīmē noklusējums? Tas nozīmē, ka, rakstot funkciju, es nedomāju par to, kā man vajadzētu nodot argumentu, es vienkārši izmantoju "noklusējumu". C++ valoda ir diezgan sarežģīta valoda, no kuras daudzi cilvēki izvairās. Un, manuprāt, sarežģītību rada ne tik daudz valodā esošo valodas konstrukciju sarežģītība (tipisks programmētājs ar tām var nesastapties), bet gan tas, ka valoda liek daudz aizdomāties: vai esmu atbrīvojies. palielināt atmiņu, vai šīs funkcijas izmantošana šeit ir dārga un tā tālāk.

Daudzi programmētāji (C, C++ un citi) neuzticas un baidās no C++, kas sāka parādīties pēc 2011. gada. Esmu dzirdējis daudz pārmetumu, ka valoda kļūst sarežģītāka, ka tajā tagad var rakstīt tikai “guru” utt. Es personīgi uzskatu, ka tas tā nav – tieši otrādi, komisija daudz laika velta tam, lai valodu padarītu draudzīgāku iesācējiem un lai programmētājiem mazāk jādomā par valodas īpatnībām. Galu galā, ja mums nav jācīnās ar valodu, mums ir laiks domāt par uzdevumu. Šie vienkāršojumi ietver viedos rādītājus, lambda funkcijas un daudz ko citu, kas parādījās valodā. Tajā pašā laikā es nenoliedzu faktu, ka tagad ir jāmācās vairāk, bet kas slikts mācībās? Vai arī citās populārās valodās, kas būtu jāapgūst, nenotiek nekādas izmaiņas?

Turklāt es nešaubos, ka būs snobi, kas atbildēs pateikt: “Tu nevēlies domāt? Pēc tam rakstiet PHP. Es pat nevēlos atbildēt šādiem cilvēkiem. Es sniegšu tikai piemēru no spēles realitātes: Starcraft pirmajā daļā, kad ēkā tiek izveidots jauns strādnieks, lai viņš sāktu iegūt derīgos izrakteņus (vai gāzi), viņš tur bija jānosūta manuāli. Turklāt katrai derīgo izrakteņu pakai bija robeža, kuru sasniedzot strādnieku skaita pieaugums bija bezjēdzīgs, un tie pat varēja traucēt viens otru, pasliktinot ražošanu. Tas tika mainīts Starcraft 2: strādnieki automātiski sāk iegūt derīgos izrakteņus (vai gāzi), un tas arī norāda, cik strādnieku pašlaik veic ieguvi un cik liela ir šīs atradnes robeža. Tas ievērojami vienkāršoja spēlētāja mijiedarbību ar bāzi, ļaujot viņam koncentrēties uz svarīgākiem spēles aspektiem: bāzes celtniecību, karaspēka uzkrāšanu un ienaidnieka iznīcināšanu. Šķiet, ka tas ir tikai lielisks jauninājums, bet kas sākās internetā! Cilvēki (kas viņi ir?) sāka kliegt, ka spēle "tiek sabojāta" un "viņi nogalināja Starcraft". Acīmredzot šādas ziņas varēja nākt tikai no “slepeno zināšanu glabātājiem” un “augsta APM adeptiem”, kuriem patika būt kādā “elites” klubā.

Tātad, atgriežoties pie mūsu tēmas, jo mazāk man ir jādomā par to, kā rakstīt kodu, jo vairāk laika man ir jādomā par tūlītējas problēmas risināšanu. Domājot par to, kuru metodi man vajadzētu izmantot - PPSC vai PPZ -, ne par kripatiņu netuvina problēmas risināšanai, tāpēc es vienkārši atsakos domāt par šādām lietām un izvēlos vienu iespēju: pārejot uz konstanti. Kāpēc? Tā kā es neredzu nekādas priekšrocības PPP vispārīgos gadījumos, un īpašie gadījumi ir jāapsver atsevišķi.

Tas ir īpašs gadījums, vienkārši, pamanījis, ka kādā metodē PPSC izrādās vājš kakls, un, nomainot transmisiju uz PPZ, mēs iegūsim būtisku veiktspējas pieaugumu, es nevilcinos izmantot PPZ. Bet pēc noklusējuma izmantošu PPSC gan parastajās funkcijās, gan konstruktoros. Un, ja iespējams, es popularizēšu šo konkrēto metodi, kur vien iespējams. Kāpēc? Jo, manuprāt, PPP veicināšanas prakse ir ļauna tāpēc, ka lauvas tiesa programmētāju nav īpaši zinoši (vai nu principā, vai vienkārši vēl nav iekļuvuši lietu šūpolēs), un viņi vienkārši seko padomiem. Turklāt, ja ir vairāki pretrunīgi padomi, viņi izvēlas to, kas ir vienkāršāks, un tas noved pie pesimisma kodā tikai tāpēc, ka kāds kaut kur kaut ko ir dzirdējis. Ak jā, šis kāds var arī iedot saiti uz Ābrahamsa rakstu, lai pierādītu, ka viņam ir taisnība. Un tad tu sēdi, lasi kodu un domā: vai tas, ka parametrs šeit tiek nodots pēc vērtības, jo programmētājs, kurš to uzrakstīja, ir nācis no Java, vienkārši lasījis daudz “gudru” rakstu, vai tiešām ir vajadzīgs tehniskā specifikācija?

PPSC ir daudz vieglāk lasīt: cilvēks skaidri zina C++ “labo formu”, un mēs ejam tālāk - skatiens nepaliek. PPSC lietošanas prakse C++ programmētājiem tiek mācīta gadiem ilgi, kāds ir iemesls no tās atteikties? Tas mani noved pie cita secinājuma: ja metodes saskarne izmanto PPP, tad ir jābūt arī komentāram, kāpēc tas tā ir. Citos gadījumos ir jāpiemēro PPSC. Protams, ir izņēmumu veidi, taču es tos šeit neminēju tikai tāpēc, ka tie ir netieši: string_view , inicializatora_saraksts , dažādi iteratori utt. Bet tie ir izņēmumi, kuru saraksts var paplašināties atkarībā no tā, kādi veidi tiek izmantoti projektā. Bet būtība paliek nemainīga kopš C++98: pēc noklusējuma mēs vienmēr izmantojam PPCS.

Attiecībā uz std::string, visticamāk, mazās virknēs atšķirības nebūs, par to mēs runāsim vēlāk.

Es jau iepriekš atvainojos par pretenciozo anotāciju par “punktu likšanu”, bet mums kaut kā jāievilina jūs rakstā)) No savas puses es centīšos nodrošināt, lai kopsavilkums joprojām atbilstu jūsu cerībām.

Īsumā par ko mēs runājam

Visi to jau zina, bet sākumā atgādināšu, kā metodes parametrus var nodot 1C. Tos var nodot “pēc atsauces” vai “pēc vērtības”. Pirmajā gadījumā metodei mēs nododam tādu pašu vērtību kā izsaukuma punktā, bet otrajā - tās kopiju.

Pēc noklusējuma programmā 1C argumenti tiek nodoti ar atsauci, un metodes parametra izmaiņas būs redzamas ārpus metodes. Šeit tālākā jautājuma izpratne ir atkarīga no tā, ko jūs saprotat ar vārdu “parametra maiņa”. Tātad, tas nozīmē atkārtotu piešķiršanu un neko vairāk. Turklāt piešķiršana var būt netieša, piemēram, platformas metodes izsaukšana, kas kaut ko atgriež izvades parametrā.

Bet, ja mēs nevēlamies, lai mūsu parametrs tiktu nodots ar atsauci, mēs varam norādīt atslēgvārdu pirms parametra Nozīme

Procedūra PēcVērtības(Vērtības parametrs) Parametrs = 2; EndProcedure parametrs = 1; PēcVērtība(parametrs); Atskaite(parametrs); // izdrukās 1

Viss darbojas, kā solīts - mainot (vai drīzāk “aizvietojot”) parametra vērtību, vērtība ārpus metodes nemainās.

Nu, kāds joks?

Interesanti brīži sākas, kad sākam kā parametrus nodot nevis primitīvus tipus (stīgas, skaitļus, datumus utt.), bet gan objektus. Šeit tiek izmantoti tādi jēdzieni kā “seklas” un “dziļas” objekta kopijas, kā arī norādes (nevis C++ terminos, bet gan kā abstrakti rokturi).

Nododot objektu (piemēram, vērtību tabulu) pēc atsauces, mēs nododam pašu rādītāja vērtību (noteiktu rokturi), kas objektu “tur” platformas atmiņā. Kad vērtība tiks nodota, platforma izveidos šī rādītāja kopiju.

Citiem vārdiem sakot, ja, nododot objektu ar atsauci, metodē mēs piešķiram parametram vērtību “Masīvs”, tad izsaukuma punktā mēs saņemsim masīvu. Ar atsauci nodotās vērtības atkārtota piešķiršana ir redzama no zvana vietas.

Procedūra ProcessValue(Parameter) Parametrs = jauns masīvs; EndProcedure Table = New ValueTable; ProcessValue(Tabula); Report(ValueType(Tabula)); // izvadīs masīvu

Ja mēs izturēsim objektu pēc vērtības, tad izsaukuma brīdī mūsu vērtību tabula netiks zaudēta.

Objekta saturs un stāvoklis

Pārejot pa vērtību, netiek kopēts viss objekts, bet tikai tā rādītājs. Objekta gadījums paliek nemainīgs. Nav nozīmes tam, kā jūs nododat objektu, pēc atsauces vai vērtības – notīrot vērtību tabulu, tiks notīrīta pati tabula. Šī tīrīšana būs redzama visur, jo... bija tikai viens objekts, un nebija nozīmes tam, kā tieši tas tika nodots metodei.

Procedūra ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Tabula.Pievienot(); ProcessValue(Tabula); Report(Tabula.Daudzums()); // izvadīs 0

Nododot objektus metodēm, platforma darbojas ar rādītājiem (nosacīti, nevis tiešie analogi no C++). Ja objekts tiek nodots ar atsauci, 1C virtuālās mašīnas atmiņas šūnu, kurā atrodas objekts, var pārrakstīt cits objekts. Ja objekts tiek nodots pēc vērtības, rādītājs tiek kopēts, un objekta pārrakstīšana neizraisa atmiņas vietas pārrakstīšanu ar sākotnējo objektu.

Tajā pašā laikā jebkuras izmaiņas Valsts objekts (tīrīšana, īpašību pievienošana utt.) maina pašu objektu, un tam nav nekāda sakara ar to, kā un kur objekts tika pārvietots. Objekta instances stāvoklis ir mainījies; uz to var būt virkne “atsauču” un “vērtību”, taču gadījums vienmēr ir vienāds. Nododot objektu metodei, mēs neizveidojam visa objekta kopiju.

Un tā vienmēr ir taisnība, izņemot...

Klienta-servera mijiedarbība

Platforma ļoti caurspīdīgi īsteno servera zvanus. Mēs vienkārši izsaucam metodi, un zem pārsega platforma serializē (pārvēršas par virkni) visus metodes parametrus, nodod tos serverim un pēc tam atgriež izvades parametrus atpakaļ klientam, kur tie tiek deserializēti un darbojas kā ja viņi nekad nebūtu bijuši nevienā serverī.

Kā jūs zināt, ne visi platformas objekti ir serializējami. Šeit pieaug ierobežojums: ne visus objektus var nodot servera metodei no klienta. Ja jūs palaižat garām objektu, kas nav serializējams, platforma sāks lietot sliktus vārdus.

  • Skaidra programmētāja nodomu deklarācija. Aplūkojot metodes parakstu, jūs varat skaidri pateikt, kuri parametri tiek ievadīti un kuri tiek izvadīti. Šo kodu ir vieglāk lasīt un uzturēt
  • Lai servera parametra “by reference” izmaiņas būtu redzamas klienta izsaukuma punktā, lpp Pati platforma noteikti atgriezīs serverim nosūtītos parametrus, izmantojot saiti uz klientu, lai nodrošinātu raksta sākumā aprakstīto uzvedību. Ja parametrs nav jāatgriež, tiks pārpildīta trafika. Lai optimizētu datu apmaiņu, parametri, kuru vērtības mums nav vajadzīgas izejā, ir jāatzīmē ar vārdu Vērtība.

Otrais punkts šeit ir ievērības cienīgs. Lai optimizētu trafiku, platforma neatgriezīs klientam parametra vērtību, ja parametrs ir atzīmēts ar vārdu Value. Tas viss ir lieliski, taču tas rada interesantu efektu.

Kā jau teicu, kad objekts tiek pārsūtīts uz serveri, notiek serializācija, t.i. tiek veikta objekta "dziļa" kopija. Un ja ir kāds vārds Nozīme objekts nepārvietosies no servera atpakaļ uz klientu. Mēs pievienojam šos divus faktus un iegūstam sekojošo:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Vērtības parametrs) Parameter.Clear(); EndProcedure &OnClient procedūra ByValueClient(Vērtības parametrs) Parameter.Clear(); EndProcedure &OnClient procedūras CheckValue() List1= New ListValues; Saraksts1.Pievienot("sveiks"); Saraksts2 = Saraksts1.Kopēt(); Saraksts3 = Saraksts1.Kopēt(); // objekts tiek kopēts pilnībā, // pārsūtīts uz serveri, pēc tam atgriezts. // saraksta dzēšana ir redzama izsaukuma punktā ByRef(List1); // objekts tiek kopēts pilnībā, // pārsūtīts uz serveri. Tas neatgriežas. // Saraksta dzēšana NAV REDZAMA ByValue(List2) izsaukšanas brīdī; // tiek kopēts tikai objekta rādītājs // saraksta notīrīšana ir redzama ByValueClient(List3) izsaukuma punktā; Report(Saraksts1.Daudzums()); Report(Saraksts2.Daudzums()); Report(Saraksts3.Daudzums()); Procedūras beigas

Kopsavilkums

Īsumā to var apkopot šādi:

  • Nodošana pēc atsauces ļauj "pārrakstīt" objektu ar pilnīgi citu objektu
  • Paiet garām vērtībai neļauj “pārrakstīt” objektu, bet būs redzamas izmaiņas objekta iekšējā stāvoklī, jo mēs strādājam ar vienu un to pašu objekta gadījumu
  • Veicot servera zvanu, darbs tiek veikts ar DAŽĀDIEM objekta gadījumiem, jo Tika veikta dziļa kopija. Atslēgvārds Nozīme neļaus servera gadījumu kopēt atpakaļ klienta instancē, un, mainot objekta iekšējo stāvokli serverī, līdzīgas izmaiņas klientā netiks veiktas.

Es ceru, ka šis vienkāršais noteikumu saraksts atvieglos strīdu risināšanu ar kolēģiem par parametru nodošanu “pēc vērtības” un “pēc atsauces”.