Transmiterea parametrilor după valoare c. Trecerea prin referință sau prin valoare? Transmiterea parametrilor prin referință

Metode de programare folosind șiruri

Scopul muncii de laborator : învață metode în limbajul C#, reguli de lucru cu date de caractere și componenta ListBox. Scrieți un program care să lucreze cu șiruri.

Metode

O metodă este un element de clasă care conține cod de program. Metoda are următoarea structură:

[atribute] [specificatori] nume tip ([parametri])

Corpul metodei;

Atributele sunt instrucțiuni speciale pentru compilator despre proprietățile unei metode. Atributele sunt rareori folosite.

Calificatorii sunt cuvinte cheie care servesc diferite scopuri, de exemplu:

· Determinarea disponibilității unei metode pentru alte clase:

o privat– metoda va fi disponibilă numai în cadrul acestei clase

o protejat– metoda va fi disponibilă și claselor de copii

o public– metoda va fi disponibilă pentru orice altă clasă care poate accesa această clasă

Indicarea disponibilității unei metode fără a crea o clasă

· Tip de setare

Tipul determină rezultatul pe care îl returnează metoda: acesta poate fi orice tip disponibil în C#, precum și cuvântul cheie void dacă rezultatul nu este necesar.

Numele metodei este identificatorul care va fi folosit pentru a apela metoda. Aceleași cerințe se aplică unui identificator ca și numelor de variabile: acesta poate consta din litere, cifre și un caracter de subliniere, dar nu poate începe cu un număr.

Parametrii sunt o listă de variabile care pot fi transmise unei metode atunci când sunt apelate. Fiecare parametru constă dintr-un tip de variabilă și un nume. Parametrii sunt separați prin virgule.

Corpul unei metode este un cod de program normal, cu excepția faptului că nu poate conține definiții ale altor metode, clase, spații de nume etc. Dacă o metodă trebuie să returneze un rezultat, atunci cuvântul cheie return trebuie să fie prezent la sfârșit cu semnificația returnată . Dacă returnarea rezultatelor nu este necesară, atunci utilizarea cuvântului cheie return nu este necesară, deși este permisă.

Un exemplu de metodă care evaluează o expresie:

public dublu Calc (dublu a, dublu b, dublu c)

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

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

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

Supraîncărcarea metodei

Limbajul C# vă permite să creați mai multe metode cu aceleași nume, dar cu parametri diferiți. Compilatorul va selecta automat metoda cea mai potrivită la construirea programului. De exemplu, puteți scrie două metode separate pentru ridicarea unui număr la o putere: un algoritm ar fi folosit pentru numere întregi, iar altul ar fi folosit pentru numere reale:

///

/// Calculați X la puterea lui Y pentru numere întregi

///

private int Pow(int X, int Y)

///

/// Calculați X la puterea lui Y pentru numere reale

///

privat dublu Pow (dublu X, dublu Y)

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

altfel dacă (Y == 0)

Acest cod este apelat în același mod, singura diferență este în parametri - în primul caz, compilatorul va apela metoda Pow cu parametri întregi, iar în al doilea - cu parametri reali:

Setări implicite

Limbajul C#, începând cu versiunea 4.0 (Visual Studio 2010), vă permite să setați valori implicite pentru anumiți parametri - astfel încât atunci când apelați o metodă să puteți omite unii dintre parametri. Pentru a face acest lucru, la implementarea metodei, parametrilor necesari ar trebui să li se atribuie o valoare direct în lista de parametri:

private void GetData(int Number, int Opțional = 5 )

Console.WriteLine("Număr: (0)", Număr);

Console.WriteLine("Opțional: (0)", Opțional);

În acest caz, puteți apela metoda după cum urmează:

GetData(10, 20);

În primul caz, parametrul Opțional va fi egal cu 20, deoarece este specificat în mod explicit, iar în al doilea caz, va fi egal cu 5, deoarece nu este specificat în mod explicit și compilatorul preia valoarea implicită.

Parametrii impliciti pot fi setati doar in partea dreapta a listei de parametri, de exemplu, o astfel de semnatura de metoda nu va fi acceptata de compilator:

private void GetData(int Opțional = 5 , int Număr)

Când parametrii sunt transferați unei metode în mod normal (fără cuvintele cheie suplimentare ref și out), orice modificare a parametrilor din cadrul metodei nu afectează valoarea acesteia în programul principal. Să presupunem că avem următoarea metodă:

private void Calc (număr int)

Se poate observa că în interiorul metodei variabila Număr, care a fost trecută ca parametru, este modificată. Să încercăm să apelăm metoda:

Console.WriteLine(n);

Pe ecran va apărea numărul 1, adică, în ciuda modificării variabilei în metoda Calc, valoarea variabilei din programul principal nu s-a schimbat. Acest lucru se datorează faptului că atunci când o metodă este apelată, a copie variabilă trecută, această variabilă se schimbă metoda. Când metoda se termină, valoarea copiilor se pierde. Această metodă de transmitere a unui parametru este numită trece prin valoare.

Pentru ca o metodă să modifice o variabilă transmisă acesteia, aceasta trebuie să fie transmisă cu cuvântul cheie ref - trebuie să fie atât în ​​semnătura metodei, cât și atunci când este apelată:

private void Calc (număr int ref)

Console.WriteLine(n);

În acest caz, pe ecran va apărea numărul 10: modificarea valorii în metodă a afectat și programul principal. Această metodă de transfer este numită trecând prin referinţă, adică Nu mai este o copie care se transmite, ci o referire la o variabilă reală din memorie.

Dacă o metodă folosește variabile prin referință doar pentru a returna valori și nu-i pasă ce a fost în ele inițial, atunci nu puteți inițializa astfel de variabile, ci le puteți transmite cu cuvântul cheie out. Compilatorul înțelege că valoarea inițială a variabilei nu este importantă și nu se plânge de lipsa inițializării:

private void Calc(out int Number)

int n; // Nu atribuim nimic!

tipul de date șir

Limbajul C# folosește tipul șir pentru a stoca șiruri. Pentru a declara (și, de regulă, a inițializa imediat) o variabilă șir, puteți scrie următorul cod:

șir a = „Text”;

șir b = „șiruri”;

Puteți efectua o operație de adăugare pe linii - în acest caz, textul unei linii va fi adăugat textului altuia:

șir c = a + " " + b; // Rezultat: șir de text

Tipul șir este de fapt un alias pentru clasa String, care vă permite să efectuați o serie de operații mai complexe pe șiruri. De exemplu, metoda IndexOf poate căuta un subșir într-un șir, iar metoda Substring returnează o porțiune a șirului de o lungime specificată, începând de la o poziție specificată:

șir a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Rezultat: 14 (numărând de la 0)

șir b = a.Substring(3, 5); // Rezultat: DEFGH

Dacă trebuie să adăugați caractere speciale unui șir, puteți face acest lucru folosind secvențe de evacuare care încep cu o bară oblică inversă:

Componenta ListBox

Componentă ListBox este o listă ale cărei elemente sunt selectate folosind tastatura sau mouse-ul. Lista de elemente este specificată de proprietate Articole. Elementele sunt un element care are propriile sale proprietăți și propriile sale metode. Metode Adăuga, RemoveAtŞi Introduce sunt folosite pentru a adăuga, șterge și insera elemente.

Obiect Articole stochează obiectele din listă. Obiectul poate fi orice clasă - datele clasei sunt convertite pentru afișare într-o reprezentare șir prin metoda ToString. În cazul nostru, șirurile vor acționa ca obiecte. Cu toate acestea, deoarece obiectul Items stochează obiecte turnate în obiect de tip, înainte de a-l folosi, trebuie să le turnați înapoi la tipul lor original, în cazul nostru șirul:

string a = (string)listBox1.Items;

Pentru a determina numărul elementului selectat, utilizați proprietatea SelectedIndex.

Îmi cer scuze anticipat pentru adnotarea pretențioasă despre „plasarea punctelor”, dar trebuie să vă atragem cumva în articol)) Din partea mea, voi încerca să mă asigur că rezumatul încă se ridică la nivelul așteptărilor dumneavoastră.

Pe scurt despre ce vorbim

Toată lumea știe deja acest lucru, dar la început vă voi reaminte cum pot fi transferați parametrii metodei în 1C. Ele pot fi transmise „prin referință” sau „prin valoare”. În primul caz, trecem metodei aceeași valoare ca la punctul de apel, iar în al doilea, o copie a acesteia.

În mod implicit, în 1C, argumentele sunt transmise prin referință, iar modificarea unui parametru în interiorul unei metode va fi vizibilă din afara metodei. Aici, înțelegerea ulterioară a întrebării depinde de ceea ce înțelegeți exact prin cuvântul „schimbare de parametru”. Deci, asta înseamnă reatribuire și nimic mai mult. Mai mult, atribuirea poate fi implicită, de exemplu, apelarea unei metode de platformă care returnează ceva în parametrul de ieșire.

Dar dacă nu dorim ca parametrul nostru să fie transmis prin referință, atunci putem specifica un cuvânt cheie înaintea parametrului Sens

Procedură ByValue(Value Parameter) Parametru = 2; Parametrul EndProcedure = 1; ByValue(Parametru); Raport (Parametru); // va imprima 1

Totul funcționează conform promisiunii - schimbarea (sau mai degrabă „înlocuirea”) valorii parametrului nu schimbă valoarea în afara metodei.

Ei bine, care-i gluma?

Momentele interesante încep când începem să trecem nu tipuri primitive (șiruri, numere, date etc.) ca parametri, ci obiecte. Aici intră în joc concepte precum copiile „superficiale” și „profunde” ale unui obiect, precum și indicatorii (nu în termeni C++, ci ca mânere abstracte).

Când trecem un obiect (de exemplu, un tabel de valori) prin referință, trecem în sine valoarea pointerului (un anumit mâner), care „ține” obiectul în memoria platformei. Când este trecută după valoare, platforma va face o copie a acestui indicator.

Cu alte cuvinte, dacă, trecând un obiect prin referință, într-o metodă atribuim parametrului valoarea „Matrice”, atunci la punctul de apel vom primi un tablou. Realocarea valorii transmise prin referință este vizibilă din locația apelului.

Procedură ProcessValue(Parameter) Parameter = New Array; EndProcedure Table = New ValueTable; ProcessValue(Tabel); Raport(ValueType(Tabel)); // va scoate un Array

Dacă trecem obiectul după valoare, atunci la punctul de apel Tabelul nostru de valori nu se va pierde.

Conținutul și starea obiectului

La trecerea după valoare, nu se copiază întregul obiect, ci doar indicatorul acestuia. Instanța obiectului rămâne aceeași. Nu contează cum treceți obiectul, prin referință sau după valoare - ștergerea tabelului de valori va șterge tabelul în sine. Această curățenie va fi vizibilă peste tot, pentru că... a existat un singur obiect și nu a contat cât de exact a fost trecut la metodă.

Procedura ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Table.Add(); ProcessValue(Tabel); Raport(Tabel.Cantitate()); // va scoate 0

La trecerea obiectelor către metode, platforma operează cu pointeri (condiționali, nu analogi directe din C++). Dacă un obiect este trecut prin referință, atunci celula de memorie a mașinii virtuale 1C în care se află obiectul poate fi suprascrisă de un alt obiect. Dacă un obiect este trecut după valoare, atunci indicatorul este copiat și suprascrierea obiectului nu are ca rezultat suprascrierea locației de memorie cu obiectul original.

În același timp, orice schimbare stat obiect (curățare, adăugare de proprietăți etc.) schimbă obiectul în sine și nu are nimic de-a face cu modul și unde a fost transferat obiectul. Starea unei instanțe de obiect s-a schimbat; pot exista o grămadă de „referințe” și „valori” la acesta, dar instanța este întotdeauna aceeași. Trecând un obiect unei metode, nu creăm o copie a întregului obiect.

Și asta este întotdeauna adevărat, cu excepția...

Interacțiunea client-server

Platforma implementează apelurile serverului foarte transparent. Numim pur și simplu o metodă, iar sub capotă platforma serializează (se transformă într-un șir) toți parametrii metodei, îi transmite serverului și apoi returnează parametrii de ieșire înapoi la client, unde sunt deserializati și trăiesc ca dacă nu ar fi fost niciodată pe vreun server.

După cum știți, nu toate obiectele platformei sunt serializabile. Aici crește limitarea: nu toate obiectele pot fi trecute la metoda server de la client. Dacă treceți un obiect neserializabil, platforma va începe să folosească cuvinte proaste.

  • O declarație explicită a intențiilor programatorului. Privind semnătura metodei, puteți spune clar care parametri sunt introduși și care sunt ieșiți. Acest cod este mai ușor de citit și întreținut
  • Pentru ca o modificare a parametrului „prin referință” de pe server să fie vizibilă la punctul de apel de pe client, p Platforma însăși va returna neapărat parametrii trecuți serverului prin link către client pentru a asigura comportamentul descris la începutul articolului. Dacă parametrul nu trebuie returnat, traficul va fi depășit. Pentru a optimiza schimbul de date, parametrii ale căror valori nu avem nevoie la ieșire ar trebui să fie marcați cu cuvântul Valoare.

Al doilea punct este de remarcat aici. Pentru a optimiza traficul, platforma nu va returna clientului valoarea parametrului dacă parametrul este marcat cu cuvântul Valoare. Toate acestea sunt grozave, dar duce la un efect interesant.

După cum am spus deja, atunci când un obiect este transferat pe server, are loc serializarea, adică. se realizează o copie „profundă” a obiectului. Și dacă există un cuvânt Sens obiectul nu va călători de la server înapoi la client. Adăugăm aceste două fapte și obținem următoarele:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= New ListValues;

List1.Add ("bună ziua");

List2 = List1.Copy();

  • List3 = List1.Copy();
  • // obiectul este copiat complet, // transferat pe server, apoi returnat.
  • // ștergerea listei este vizibilă la punctul de apel ByRef(List1); Sens// obiectul este copiat complet, // transferat pe server. Nu se întoarce.

// Ștergerea listei NU este VIZIBILĂ în momentul apelării ByValue(List2);

Când am început să programez în C++ și am studiat intens cărți și articole, am dat invariabil de același sfat: dacă trebuie să trecem un obiect unei funcții care nu ar trebui să se schimbe în funcție, atunci ar trebui să fie întotdeauna transmis. prin referire la o constantă(PPSK), cu excepția cazurilor în care trebuie să trecem fie un tip primitiv, fie o structură similară ca dimensiune cu acestea. Deoarece De mai bine de 10 ani de programare în C++, am întâlnit foarte des acest sfat (și eu însumi l-am dat de mai multe ori), a fost de mult „absorbit” în mine - trec automat toate argumentele prin referire la o constantă . Dar timpul trece și deja au trecut 7 ani de când am avut C++11 la dispoziție cu semantica lui de mișcare, în legătură cu care aud din ce în ce mai multe voci care pun la îndoială vechea dogma bună. Mulți încep să susțină că trecerea prin referire la o constantă este un lucru din trecut și acum este necesar trece prin valoare(PPZ). Ce se află în spatele acestor conversații, precum și ce concluzii putem trage din toate acestea, vreau să discut în acest articol.

Înțelepciunea cărții

Pentru a înțelege ce regulă ar trebui să respectăm, vă sugerez să apelați la cărți. Cărțile sunt o sursă excelentă de informații pe care nu suntem obligați să o acceptăm, dar care merită cu siguranță ascultată. Și vom începe cu istoria, cu originile. Nu voi afla cine a fost primul apologe al PPSC, voi da pur și simplu ca exemplu cartea care personal a avut cea mai mare influență asupra mea în problema utilizării PPSC.

Mayers

Bine, aici avem o clasă în care toți parametrii sunt trecuți prin referință, există probleme cu această clasă? Din păcate, există, iar această problemă se află la suprafață. Avem 2 entități funcționale în clasa noastră: prima ia o valoare în etapa de creare a obiectului, iar a doua vă permite să schimbați o valoare setată anterior. Avem două entități, dar patru funcții. Acum imaginați-vă că putem avea nu 2 entități similare, ci 3, 5, 6, ce atunci? Atunci ne vom confrunta cu balonare severă de cod. Prin urmare, pentru a nu crea o masă de funcții, a existat o propunere de a abandona cu totul legăturile în parametri:

Șablon Class Holder ( public: explicit Holder(T value): m_Value(move(value)) ( ) void setValue(T value) ( ​​​​m_Value = move(value); ) const T& value() const noexcept ( return m_Value; ) privat: T m_Value);

Primul avantaj care vă atrage imediat atenția este că există mult mai puțin cod. Există chiar mai puțin decât în ​​prima versiune, datorită eliminării const și & (deși au adăugat move ). Dar întotdeauna am fost învățați că trecerea prin referință este mai productivă decât trecerea după valoare! Așa era înainte de C++11 și cum este încă, dar acum, dacă ne uităm la acest cod, vom vedea că nu există mai multă copiere aici decât în ​​prima versiune, cu condiția ca T să aibă un constructor de mutare. Aceste. PPSC în sine a fost și va fi mai rapid decât PPZ, dar codul folosește cumva referința transmisă și adesea acest argument este copiat.

Cu toate acestea, aceasta nu este toată povestea. Spre deosebire de prima variantă, unde avem doar copiere, aici adăugăm și mișcare. Dar mutarea este o operațiune ieftină, nu? Pe această temă, cartea Mayers pe care o luăm în considerare are și un capitol („Punctul 29”), care se intitulează: „Să presupunem că operațiunile de mutare nu sunt prezente, nu sunt ieftine și nu sunt folosite”. Ideea principală ar trebui să fie clară din titlu, dar dacă doriți detalii, asigurați-vă că o citiți - nu mă voi opri asupra ei.

Ar fi potrivit să efectuăm aici o analiză comparativă completă a primei și ultimei metode, dar nu aș dori să mă abat de la carte, așa că vom amâna analiza pentru alte secțiuni și aici vom continua să luăm în considerare argumentele lui Scott. Deci, în afară de faptul că a treia opțiune este în mod evident mai scurtă decât a doua, care vede Scott ca fiind avantajul PPZ față de PPSC în codul modern?

O vede în faptul că în cazul trecerii unei valori r, i.e. unii numesc astfel: Holder holder(string("me")); , varianta cu PPSC ne va da copiere, iar optiunea cu PPZ ne va da miscare. Pe de altă parte, dacă transferul este astfel: Holder holder(someLvalue); , atunci PPZ-ul pierde definitiv din cauza faptului că va efectua atât copierea, cât și mutarea, în timp ce în versiunea cu PPSC va exista o singură copie. Aceste. se dovedește că PPZ, dacă luăm în considerare pur eficiență, este un fel de compromis între cantitatea de cod și suportul „complet” (prin &&) pentru semantica mișcării.

De aceea, Scott și-a formulat sfatul atât de atent și îl promovează cu atâta grijă. Chiar mi s-a părut că a adus-o în discuție fără tragere de inimă, parcă sub presiune: nu a putut să nu pună discuții pe această temă în carte, pentru că... s-a discutat destul de larg, iar Scott a fost întotdeauna un colecționar de experiență colectivă. În plus, dă foarte puține argumente în apărarea PPZ, dar pune multe dintre cele care pun în discuție această „tehnică”. Ne vom uita la argumentele sale împotriva în secțiunile ulterioare, dar aici vom repeta pe scurt argumentul pe care Scott îl face în apărarea PPP (adăugând mental „dacă obiectul suportă mișcarea și este ieftin”): vă permite să evitați copierea atunci când treceți o expresie rvalue ca argument al funcției. Dar destul de chinuitoare cartea lui Meyers, să trecem la o altă carte.

Apropo, dacă cineva a citit cartea și este surprins că nu includ aici o opțiune cu ceea ce Mayers numea referințe universale - cunoscute acum ca referințe de redirecționare - atunci acest lucru este ușor de explicat. Mă gândesc doar la PPZ și PPSC, pentru că... Consider că este o formă proastă să introduc funcții șablon pentru metode care nu sunt șabloane doar de dragul de a sprijini trecerea prin referință a ambelor tipuri (rvalue/lvalue). Ca să nu mai vorbim de faptul că codul iese diferit (nu mai este constantă) și aduce cu el și alte probleme.

Josattis și compania

Ultima carte la care ne vom uita este „Șabloane C++”, care este și cea mai recentă dintre toate cărțile menționate în acest articol. A fost publicată la sfârșitul anului 2017 (și 2018 este indicat în interiorul cărții). Spre deosebire de alte cărți, aceasta este în întregime dedicată tiparelor și nu sfaturilor (cum ar fi Mayers) sau C++ în general, precum Stroustrup. Prin urmare, avantajele/contra sunt luate în considerare aici din punctul de vedere al scrierii șabloanelor.

Un întreg capitol 7 este dedicat acestui subiect, care poartă titlul elocvent „După valoare sau prin referință?”. În acest capitol, autorii descriu pe scurt, dar succint, toate metodele de transmitere cu toate avantajele și dezavantajele lor. O analiză a eficienței nu este, practic, făcută aici și se ia de la sine înțeles că PPSC va fi mai rapid decât PPZ. Dar cu toate acestea, la sfârșitul capitolului autorii recomandă utilizarea PPP implicit pentru funcțiile șablon. De ce? Deoarece folosind o legătură, parametrii șablonului sunt afișați complet, iar fără o legătură sunt „degradați”, ceea ce are un efect benefic asupra procesării matricelor și a literalelor șir. Autorii cred că, dacă pentru un anumit tip de PPP se dovedește a fi ineficient, atunci puteți utiliza întotdeauna std::ref și std::cref . Acesta este un sfat, sincer să fiu, ați văzut mulți oameni care doresc să folosească funcțiile de mai sus?

Ce sfătuiesc ei cu privire la PPSC? Ei recomandă utilizarea PPSC atunci când performanța este critică sau există altele cu greutate motive pentru a nu folosi PPP. Desigur, aici vorbim doar despre codul standard, dar acest sfat contrazice direct tot ceea ce programatorii au fost predat timp de un deceniu. Acesta nu este doar un sfat pentru a considera PPP ca o alternativă - nu, acesta este un sfat pentru a face din PPSC o alternativă.

Aceasta încheie turul nostru de carte, deoarece... Nu cunosc alte cărți pe care să le consultăm pe această problemă. Să trecem la alt spațiu media.

Înțelepciunea rețelei

Deoarece Trăim în era internetului, atunci nu ar trebui să te bazezi doar pe înțelepciunea cărților. Mai mult, mulți autori care scriau cărți acum scriu pur și simplu bloguri și au abandonat cărțile. Unul dintre acești autori este Herb Sutter, care în mai 2013 a publicat un articol pe blogul său „GotW #4 Solution: Class Mechanics”, care, deși nu este în întregime dedicat problemei pe care o acoperim, încă o atinge.

Deci, în versiunea originală a articolului, Sutter a repetat pur și simplu vechea înțelepciune: „treceți parametrii prin referire la o constantă”, dar nu vom mai vedea această versiune a articolului, deoarece Articolul conține sfatul opus: „ Dacă parametrul va fi tot copiat, apoi trece-l după valoare.” Din nou notoriul „dacă”. De ce a schimbat Sutter articolul și de unde am știut despre el? Din comentarii. Apropo, citiți comentariile la articolul său, sunt mai interesante și mai utile decât articolul în sine. Adevărat, după ce a scris articolul, Sutter și-a schimbat în sfârșit părerea și nu mai dă astfel de sfaturi. Schimbarea de opinie se regăsește în discursul său la CppCon din 2014: „Back to the Basics! Elementele esențiale ale stilului C++ modern”. Asigurați-vă că vă uitați, vom trece la următorul link de internet.

Și în continuare avem principala resursă de programare a secolului 21: StackOverflow. Sau mai degrabă răspunsul, cu numărul de reacții pozitive depășind 1700 la momentul scrierii acestui articol. Întrebarea este: care este expresia de copiere și schimb? , și, așa cum ar trebui să sugereze titlul, nu este tocmai în subiectul pe care îl analizăm. Dar în răspunsul său la această întrebare, autorul atinge și o temă care ne interesează. El sfătuiește, de asemenea, să folosești PPZ „dacă argumentul va fi copiat oricum” (este timpul să introduci și o abreviere pentru aceasta, Dumnezeule). Și, în general, acest sfat pare destul de adecvat, în cadrul răspunsului său și al operatorului= discutat acolo, dar autorul își are libertatea de a da astfel de sfaturi într-o manieră mai largă, și nu doar în acest caz particular. Mai mult decât atât, el merge mai departe decât toate sfaturile pe care le-am discutat anterior și solicită să faceți acest lucru chiar și în codul C++03! Ce l-a determinat pe autor să tragă astfel de concluzii?

Aparent, autorul răspunsului și-a inspirat principala inspirație dintr-un articol al unui alt autor de cărți și dezvoltator part-time al lui Boost.MPL - Dave Abrahams. Articolul se numește „Vrei viteză? Treci prin valoare.” , și a fost publicat încă din august 2009, i.e. Cu 2 ani înainte de adoptarea C++11 și introducerea semanticii de mișcare. Ca și în cazurile anterioare, recomand cititorului să citească singur articolul, dar voi da principalele argumente (există, de fapt, un singur argument) pe care Dave le dă în favoarea PPZ: trebuie să utilizați PPZ , deoarece optimizarea „skip copy” funcționează bine cu ea ( copy elision), care este absentă în PPSC. Dacă citiți comentariile la articol, puteți vedea că sfaturile pe care le promovează nu sunt universale, lucru pe care autorul însuși le confirmă atunci când răspunde criticilor comentatorilor. Cu toate acestea, articolul conține sfaturi explicite (orientări) pentru a utiliza PPP dacă argumentul va fi oricum copiat. Apropo, dacă cineva este interesat, puteți citi articolul „Vrei viteză? Nu trece (întotdeauna) prin valoare.” . După cum ar trebui să indice titlul, acest articol este un răspuns la articolul lui Dave, așa că dacă îl citiți pe primul, asigurați-vă că îl citiți și pe acesta!

Din păcate (din fericire pentru unii), astfel de articole și (cu atât mai mult) răspunsuri populare pe site-uri populare dau naștere la utilizarea masivă a tehnicilor dubioase (un exemplu banal) pur și simplu pentru că necesită mai puțină scriere, iar vechea dogmă nu mai este de nezdruncinat - Vă puteți referi oricând la „acel sfat popular” dacă sunteți împins la perete. Acum vă sugerez să vă familiarizați cu ce ne oferă diverse resurse cu recomandări pentru scrierea codului.

Deoarece Deoarece diverse standarde și recomandări sunt acum postate online, am decis să clasific această secțiune drept „înțelepciune de rețea”. Așadar, aici aș dori să vorbesc despre două surse, al căror scop este să îmbunătățească codul programatorilor C++, oferindu-le sfaturi (ghiduri) despre cum să scrie chiar acest cod.

Primul set de reguli pe care vreau să-l iau în considerare a fost ultimul care m-a forțat să mă apuc de acest articol. Acest set face parte din utilitarul clang-tidy și nu există în afara acestuia. La fel ca tot ce are legătură cu clang, acest utilitar este foarte popular și a primit deja integrare cu CLion și Resharper C++ (așa am dat peste el). Deci, clang-tydy conține regula modernize-pass-by-value, care funcționează pe constructori care acceptă argumente prin PPSK. Această regulă sugerează să înlocuim PPSC cu PPZ. Mai mult, la momentul redactării articolului, descrierea acestei reguli conține o remarcă că această regulă la revedere funcționează doar pentru constructori, dar ei (cine sunt ei?) vor accepta cu plăcere ajutorul celor care extind această regulă și la alte entități. Acolo, în descriere, există un link către articolul lui Dave - este clar de unde vin picioarele.

În cele din urmă, pentru a încheia această trecere în revistă a înțelepciunii și a opiniilor cu autoritate ale altora, vă sugerez să vă uitați la ghidurile oficiale pentru scrierea codului C++: C++ Core Guidelines, ai căror editori principali sunt Herb Sutter și Bjarne Stroustrup (nu-i rău, nu?). Deci, aceste recomandări conțin următoarea regulă: „Pentru parametrii „în”, treceți tipurile copiate ieftin după valoare și altele prin referire la const”, care repetă complet vechea înțelepciune: PPSK peste tot și PPP pentru obiecte mici. Acest sfat prezintă câteva alternative de luat în considerare. în cazul în care trecerea argumentului necesită optimizare. Dar PPZ nu este inclus în lista de alternative!

Întrucât nu am alte surse demne de atenție, îmi propun să trecem la o analiză directă a ambelor metode de transmitere.

Analiză

Întregul text precedent este scris într-un mod oarecum neobișnuit pentru mine: prezint opiniile altora și chiar încerc să nu le exprim pe ale mele (știu că iese prost). În mare parte datorită faptului că opiniile altora, iar scopul meu a fost să fac o scurtă trecere în revistă a acestora, am amânat o analiză detaliată a anumitor argumente pe care le-am găsit la alți autori. În această secțiune, nu mă voi referi la autorități și voi da opinii aici vom analiza câteva avantaje și dezavantaje obiective ale PPSC și PPZ, care vor fi asezonate cu percepția mea subiectivă. Desigur, unele dintre cele discutate mai devreme vor fi repetate, dar, din păcate, aceasta este structura acestui articol.

PPP are un avantaj?

Așadar, înainte de a lua în considerare argumentele pro și contra, îmi propun să ne uităm la ce și în ce cazuri avantajul pe care ni-l oferă trecerea prin valoare. Să presupunem că avem o clasă ca aceasta:

Clasa CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotValuer (Și m_ValuerByValuer) uerAndNotMover; void setRvaluer (Cont&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Deși în scopul acestui articol ne interesează doar primele două funcții, am inclus patru opțiuni doar pentru a le folosi ca contrast.

Clasa Accounter este o clasă simplă care numără de câte ori a fost copiată/mută. Și în clasa CopyMover am implementat funcții care ne permit să luăm în considerare următoarele opțiuni:

    în mișcare argument trecut.

    Treci după valoare, urmat de copierea argument trecut.

Acum, dacă trecem o valoare l fiecăreia dintre aceste funcții, de exemplu astfel:

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

atunci obținem următoarele rezultate:

Câștigătorul evident este PPSC, pentru că... oferă o singură copie, în timp ce PPZ oferă o copie și o mișcare.

Acum să încercăm să transmitem o valoare r:

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

Obținem următoarele:

Nu există un câștigător clar aici, pentru că... atât PPZ cât și PPSK au câte o operațiune, dar datorită faptului că PPZ folosește mișcarea, iar PPSK folosește copierea, putem da victoria lui PPZ.

Dar experimentele noastre nu se termină aici, să adăugăm următoarele funcții pentru a simula un apel indirect (cu trecerea ulterioară a argumentului):

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

Le vom folosi exact la fel ca și fără ele, așa că nu voi repeta codul (căutați în depozit dacă este necesar). Deci, pentru lvalue, rezultatele ar fi astfel:

Rețineți că PPSC mărește decalajul față de PPZ, rămânând cu o singură copie, în timp ce PPZ are deja până la 3 operațiuni (încă o mișcare)!

Acum trecem valoarea r și obținem următoarele rezultate:

Acum PPZ are 2 mișcări, iar PPSC mai are un exemplar. Este acum posibil să nominalizezi PPZ drept câștigător? Nu, pentru că dacă o mișcare ar trebui să fie cel puțin nu mai rea decât o copie, nu mai putem spune același lucru despre 2 mișcări. Prin urmare, nu va exista niciun câștigător în acest exemplu.

Ei ar putea obiecta la mine: „Autore, ai o părere părtinitoare și aduci ceea ce este benefic pentru tine. Chiar și 2 mișcări vor fi mai ieftine decât copierea!” Nu pot fi de acord cu această afirmație În întregime, pentru că Cât de mult este mutarea mai rapidă decât copierea depinde de clasa specifică, dar ne vom uita la mutarea „ieftină” într-o secțiune separată.

Aici am atins un lucru interesant: am adăugat un apel indirect, iar PPP a adăugat exact o operațiune în „greutate”. Cred că nu este nevoie să ai o diplomă de la MSTU pentru a înțelege că, cu cât avem mai multe apeluri indirecte, cu atât se vor efectua mai multe operațiuni la utilizarea PPZ, în timp ce pentru PPSC numărul va rămâne neschimbat.

Tot ce s-a discutat mai sus este puțin probabil să fie o revelație pentru cineva, s-ar putea să nu fi efectuat experimente - toate aceste numere ar trebui să fie evidente pentru majoritatea programatorilor C++ la prima vedere. Adevărat, un punct mai merită clarificat: de ce, în cazul rvalue, PZ-ul nu are o copie (sau o altă mutare), ci o singură mișcare.

Ei bine, ne-am uitat la diferența de transmisie dintre PPZ și PPSC observând direct numărul de copii și mutări. Deși este evident că avantajul PPZ față de PPSC chiar și în astfel de exemple simple este, pentru a spune ușor Nu Evident, încă, puțin cam strâmb, fac următoarea concluzie: dacă încă vom copia argumentul funcției, atunci are sens să luăm în considerare trecerea argumentului funcției după valoare. De ce am tras această concluzie? Pentru a trece fără probleme la următoarea secțiune.

Daca copiem...

Așadar, ajungem la proverbialul „dacă”. Majoritatea argumentelor pe care le-am întâlnit nu au cerut implementarea universală a PPP în loc de PPSC, ei au cerut doar să se facă acest lucru „dacă argumentul este copiat oricum”. Este timpul să ne dăm seama ce este în neregulă cu acest argument.

Vreau să încep cu o mică descriere a modului în care scriu codul. În ultimul timp, procesul meu de codificare a devenit din ce în ce mai asemănător TDD, adică. scrierea oricărei metode de clasă începe cu scrierea unui test în care apare această metodă. În consecință, când încep să scriu un test și creez o metodă după scrierea testului, încă nu știu dacă voi copia argumentul. Desigur, nu toate funcțiile sunt create în acest fel, de multe ori, chiar și în procesul de scriere a unui test, știi exact ce fel de implementare va exista; Dar asta nu se întâmplă întotdeauna!

Cineva mi-ar putea obiecta că nu contează modul în care a fost scrisă inițial metoda, putem schimba modul în care transmitem argumentul atunci când metoda a luat contur și ne este complet clar ce se întâmplă acolo (adică dacă avem copiere sau nu). Sunt parțial de acord cu asta - într-adevăr, puteți face acest lucru, dar acest lucru ne implică într-un fel de joc ciudat în care trebuie să schimbăm interfețele doar pentru că implementarea s-a schimbat. Ceea ce ne duce la următoarea dilemă.

Se pare că modificăm (sau chiar planificăm) interfața în funcție de modul în care va fi implementată. Nu mă consider un expert în OOP și alte calcule teoretice ale arhitecturii software, dar astfel de acțiuni contrazic în mod clar regulile de bază atunci când implementarea nu ar trebui să afecteze interfața. Desigur, anumite detalii de implementare (fie că sunt caracteristici ale limbajului sau ale platformei țintă) încă mai curg prin interfață într-un fel sau altul, dar ar trebui să încercați să reduceți, nu să creșteți numărul de astfel de lucruri.

Ei bine, Dumnezeu să-l binecuvânteze, să mergem pe această cale și să schimbăm totuși interfețele în funcție de ceea ce facem în implementare în ceea ce privește copierea argumentului. Să presupunem că am scris această metodă:

Void setName(Nume nume) ( m_Name = mutare(nume); )

și am trimis modificările noastre în depozit. Pe măsură ce timpul a trecut, produsul nostru software a dobândit noi funcționalități, noi cadre au fost integrate și a apărut sarcina de a informa lumea exterioară despre schimbările din clasa noastră. Aceste. Vom adăuga un mecanism de notificare la metoda noastră, să fie ceva similar cu semnalele Qt:

Void setName(Nume Nume) (m_Name = mutare(nume); emit nameChanged(m_Name); )

Există vreo problemă cu acest cod? Mânca. Pentru fiecare apel către setName trimitem un semnal, astfel încât semnalul va fi trimis chiar și când sens m_Name nu s-a schimbat. Pe lângă problemele de performanță, această situație poate duce la o buclă infinită din cauza codului care primește notificarea de mai sus ajungând cumva să apeleze setName . Pentru a evita toate aceste probleme, astfel de metode arată cel mai adesea cam așa:

Void setName(Nume nume) ( if(name == m_Name) return; m_Name = muta (nume); emit nameChanged(m_Name); )

Am scăpat de problemele descrise mai sus, dar acum regula noastră „dacă copiem oricum...” a eșuat - nu mai există copierea necondiționată a argumentului, acum o copiem doar dacă se schimbă! Deci ce ar trebui să facem acum? Schimbați interfața? Bine, să schimbăm interfața clasei din cauza acestei remedieri. Ce se întâmplă dacă clasa noastră ar moșteni această metodă de la o interfață abstractă? Hai să-l schimbăm și acolo! Sunt multe schimbări pentru că s-a schimbat implementarea?

Din nou, s-ar putea să mă opună, spun ei, autorul, de ce încerci să economisești bani pe chibrituri când această condiție va funcționa acolo? Da, majoritatea apelurilor vor fi false! Există vreo încredere în asta? Unde? Și dacă am decis să economisesc la meciuri, nu a fost chiar faptul că am folosit PPZ o consecință a unor astfel de economii? Eu doar continui „linia de partid” care pledează pentru eficiență.

Constructorii

Să trecem pe scurt peste constructori, mai ales că există o regulă specială pentru aceștia în clang-tidy, care încă nu funcționează pentru alte metode/funcții. Să presupunem că avem o clasă ca aceasta:

Clasa JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) privat: string m_JustString; );

Evident, parametrul este copiat și clang-tidy ne va spune că ar fi o idee bună să rescriem constructorul la asta:

JustClass(șir justString): m_JustString(mutare(justString)) ( )

Și, sincer vorbind, îmi este dificil să mă cert aici - la urma urmei, cu adevărat copiam întotdeauna. Și cel mai adesea, când trecem ceva printr-un constructor, îl copiem. Dar mai des nu înseamnă întotdeauna. Iată un alt exemplu:

Clasa TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = mutare(start); m_End = mutare(sfârșit); ) privat: DateTime m_Start; DateTime m_End; );

Aici nu copiam întotdeauna, ci doar atunci când datele sunt prezentate corect. Desigur, în marea majoritate a cazurilor acesta va fi cazul. Dar nu întotdeauna.

Puteți da un alt exemplu, dar de data aceasta fără cod. Imaginați-vă că aveți o clasă care acceptă un obiect mare. Clasa există de mult timp, iar acum este timpul să-i actualizăm implementarea. Ne dăm seama că nu avem nevoie de mai mult de jumătate dintr-o unitate mare (care a crescut de-a lungul anilor) și poate chiar mai puțin. Putem face ceva în privința asta prin trecerea prin valoare? Nu, nu vom putea face nimic, deoarece o copie va fi în continuare creată. Dar dacă am folosi PPSC, am schimba pur și simplu ceea ce facem interior proiectant. Și acesta este punctul cheie: folosind PPSC controlăm ce și când se întâmplă în implementarea funcției noastre (constructor), dar dacă folosim PPZ, atunci pierdem orice control asupra copierii.

Ce poți scoate din această secțiune? Faptul că argumentul „dacă copiem oricum...” este foarte controversat, pentru că Nu știm întotdeauna ce vom copia și, chiar și atunci când știm, de multe ori nu suntem siguri că acest lucru va continua în viitor.

Mutarea este ieftină

Chiar din momentul în care a apărut semantica mișcării, ea a început să aibă o influență serioasă asupra modului în care este scris codul C++ modern, iar de-a lungul timpului această influență nu a făcut decât să se intensifice: nu este de mirare, pentru că mișcarea este atât de mare. ieftin comparativ cu copierea. Dar este acest lucru adevărat? Este adevărat că mișcarea este Întotdeauna operatie ieftina? Acesta este ceea ce vom încerca să descoperim în această secțiune.

Obiect mare binar

Să începem cu un exemplu banal, să presupunem că avem următoarea clasă:

Struct Blob (std::array date; );

Comun blob(BDO, engleză BLOB), care poate fi folosit într-o varietate de situații. Să ne uităm la ce ne va costa să trecem prin referință și după valoare. BDO-ul nostru va fi folosit cam așa:

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

Și vom numi aceste funcții astfel:

Const Blob blob(); depozitare; stocare.setBlobByRef(blob); stocare.setBlobByVal(blob);

Codul pentru alte exemple va fi identic cu acesta, doar cu nume și tipuri diferite, așa că nu îl voi da pentru exemplele rămase - totul este în depozit.

Înainte de a trece la măsurători, să încercăm să prezicem rezultatul. Deci avem un 4 KB std::array pe care dorim să îl stocăm într-un obiect din clasa Storage. După cum am aflat mai devreme, pentru PPSC vom avea un exemplar, în timp ce pentru PPZ vom avea un exemplar și o mutare. Pe baza faptului că este imposibil să mutați matricea, vor exista 2 copii pentru PPZ, față de una pentru PPSC. Aceste. ne putem aștepta la o dublă superioritate în performanță pentru PPSC.

Acum să aruncăm o privire la rezultatele testului:

Aceasta și toate testele ulterioare au fost executate pe aceeași mașină folosind MSVS 2017 (15.7.2) și cu steag-ul /O2.

Practica a coincis cu ipoteza - trecerea după valoare este de 2 ori mai scumpă, deoarece pentru o matrice, mutarea este complet echivalentă cu copierea.

Linia

Să ne uităm la un alt exemplu, un std::string obișnuit. La ce ne putem aștepta? Știm (am discutat despre asta în articol) că implementările moderne disting între două tipuri de șir: scurt (aproximativ 16 caractere) și lung (cele care sunt mai lungi decât scurte). Pentru cele scurte, se folosește un buffer intern, care este o matrice C obișnuită de char , dar cele lungi vor fi deja plasate pe heap. Nu ne interesează rândurile scurte, pentru că... rezultatul va fi același ca și cu BDO, așa că să ne concentrăm pe linii lungi.

Deci, având un șir lung, este evident că mutarea acestuia ar trebui să fie destul de ieftină (doar mișcați indicatorul), așa că puteți conta pe faptul că mutarea șirului nu ar trebui să afecteze deloc rezultatele, iar PPZ ar trebui să dea un rezultat nu mai rău decât PPSC. Să o verificăm în practică și să obținem următoarele rezultate:

Vom trece la explicarea acestui „fenomen”. Deci, ce se întâmplă când copiem un șir existent într-un șir deja existent? Să ne uităm la un exemplu banal:

String primul (64, „C”); șir secundă (64, „N”); //... al doilea = primul;

Avem două șiruri de 64 de caractere, astfel încât bufferul intern este insuficient la crearea lor, rezultând că ambele șiruri sunt alocate pe heap. Acum copiam de la primul la al doilea. Deoarece Dimensiunile rândurilor noastre sunt aceleași, evident că există suficient spațiu alocat în secundă pentru a găzdui toate datele de la primul, deci secund = primul;

va fi o memcpy banala, nimic mai mult. Dar dacă ne uităm la un exemplu ușor modificat:

atunci nu va mai exista un apel la operator= , ci va fi apelat constructorul de copiere. Deoarece Deoarece avem de-a face cu un constructor, nu există nicio memorie în el. Mai întâi trebuie selectat și abia apoi copiat mai întâi. Aceste. aceasta este alocarea memoriei și apoi memcpy. După cum știm tu și cu mine, alocarea memoriei pe heap-ul global este de obicei o operațiune costisitoare, așa că copierea din al doilea exemplu va fi mai costisitoare decât copierea din primul. Alocare de memorie mai scumpă pe heap.

Ce legătură are asta cu subiectul nostru? Cel mai direct, pentru că primul exemplu arată exact ce se întâmplă cu PPSC, iar al doilea arată ce se întâmplă cu PPZ: pentru PPZ se creează întotdeauna un rând nou, în timp ce pentru PPSC se reutiliza cel existent. Ați văzut deja diferența de timp de execuție, așa că nu este nimic de adăugat aici.

Aici ne confruntăm din nou cu faptul că atunci când folosim PPP, lucrăm în afara contextului și, prin urmare, nu putem folosi toate beneficiile pe care le poate oferi. Și dacă mai devreme am raționat în termeni de schimbări teoretice viitoare, aici observăm un eșec foarte concret în productivitate.

Desigur, cineva mi-ar putea obiecta că șirul este separat și majoritatea tipurilor nu funcționează în acest fel. La care pot răspunde la următoarele: tot ceea ce este descris mai devreme va fi adevărat pentru orice container care alocă imediat memorie în heap pentru un pachet de elemente. De asemenea, cine știe ce alte optimizări sensibile la context sunt folosite în alte tipuri?

Ce ar trebui să scoți din această secțiune? Faptul că chiar dacă mutarea este cu adevărat ieftină nu înseamnă că înlocuirea copierii cu copy+moving va da întotdeauna un rezultat comparabil din punct de vedere al performanței.

Tip complex

În cele din urmă, să ne uităm la un tip care va consta din mai multe obiecte. Fie aceasta o clasă Person, care constă din date inerente unei persoane. De obicei, acesta este prenumele, numele, codul poștal etc. Poți să te gândești la toate acestea ca pe niște șiruri de caractere și să presupui că șirurile pe care le pui în câmpurile clasei Person sunt probabil să fie scurte. Deși cred că în viața reală, măsurarea corzilor scurte va fi cea mai utilă, totuși ne vom uita la corzi de diferite dimensiuni pentru a oferi o imagine mai completă.

Voi folosi și Persoană cu 10 câmpuri, dar pentru aceasta nu voi crea 10 câmpuri direct în corpul clasei. Implementarea lui Person ascunde un container în adâncurile sale - acest lucru face mai convenabilă schimbarea parametrilor de testare, practic fără a vă abate de la modul în care ar funcționa dacă Person ar fi o clasă reală. Cu toate acestea, implementarea este disponibilă și puteți oricând să verificați codul și să-mi spuneți dacă am greșit ceva.

Deci, să mergem: Persoană cu 10 câmpuri de tip șir , pe care le transferăm folosind PPSC și PPZ în Storage :

După cum puteți vedea, avem o diferență uriașă de performanță, care nu ar trebui să surprindă cititorii după secțiunile anterioare. De asemenea, cred că clasa Persoană este suficient de „reală” încât astfel de rezultate nu vor fi respinse ca abstracte.

Apropo, când pregăteam acest articol, am pregătit un alt exemplu: o clasă care folosește mai multe obiecte std::function. După ideea mea, ar fi trebuit să arate și un avantaj în performanța PPSC față de PPZ, dar s-a dovedit exact invers! Dar nu dau acest exemplu aici nu pentru că nu mi-au plăcut rezultatele, ci pentru că nu am avut timp să îmi dau seama de ce au fost obținute astfel de rezultate. Cu toate acestea, există cod în depozit (Imprimante), teste - de asemenea, dacă cineva dorește să-și dea seama, aș fi bucuros să aud despre rezultatele cercetării. Plănuiesc să revin la acest exemplu mai târziu, iar dacă nimeni nu publică aceste rezultate înaintea mea, atunci le voi lua în considerare într-un articol separat.

Rezultate

Așa că ne-am uitat la diferitele avantaje și dezavantaje ale trecerii prin valoare și trecerii prin referire la o constantă. Am analizat câteva exemple și am analizat performanța ambelor metode în aceste exemple. Desigur, acest articol nu poate și nu este exhaustiv, dar, în opinia mea, conține suficiente informații pentru a lua o decizie independentă și în cunoștință de cauză cu privire la care metodă este cea mai bună de utilizat. Cineva poate obiecta: „De ce să folosiți o singură metodă, să începem de la sarcină!” Deși sunt de acord cu această teză în general, nu sunt de acord cu ea în această situație. Cred că poate exista o singură modalitate de a transmite argumente într-o limbă, care este folosit implicit.

Ce înseamnă implicit? Aceasta înseamnă că atunci când scriu o funcție, nu mă gândesc la cum să trec argumentul, folosesc doar „default”. Limbajul C++ este un limbaj destul de complex pe care mulți oameni îl evită. Și după părerea mea, complexitatea este cauzată nu atât de complexitatea constructelor de limbaj care există în limbaj (un programator tipic poate să nu le întâlnească niciodată), cât de faptul că limbajul te face să te gândești mult: am eliberat până la memorie, este scump să folosești această funcție aici, etc.

Mulți programatori (C, C++ și alții) sunt neîncrezători și se tem de C++ care a început să apară după 2011. Am auzit multe critici că limbajul devine din ce în ce mai complex, că doar „gurus” pot scrie acum în el etc. Personal, cred că nu este așa - dimpotrivă, comitetul dedică mult timp pentru a face limbajul mai prietenos pentru începători și astfel încât programatorii trebuie să se gândească mai puțin la caracteristicile limbajului. La urma urmei, dacă nu trebuie să ne luptăm cu limba, atunci avem timp să ne gândim la sarcină. Aceste simplificări includ indicatoare inteligente, funcții lambda și multe altele care au apărut în limbaj. În același timp, nu neg faptul că acum trebuie să studiem mai mult, dar ce este rău în a studia? Sau nu au loc schimbări în alte limbi populare care trebuie învățate?

În plus, nu am nicio îndoială că vor exista snobi care vor putea spune ca răspuns: „Nu vrei să gândești? Apoi scrieți în PHP.” Nici nu vreau să răspund unor asemenea oameni. Voi da doar un exemplu din realitatea jocului: în prima parte a Starcraft, când un nou muncitor este creat într-o clădire, pentru ca acesta să înceapă să extragă minerale (sau gaz), trebuia să fie trimis manual acolo. Mai mult, fiecare pachet de minerale avea o limită, la atingerea căreia creșterea muncitorilor era inutilă, ba chiar se puteau interfera între ele, înrăutățind producția. Acest lucru a fost schimbat în Starcraft 2: lucrătorii încep automat să exploateze minerale (sau gaz) și indică, de asemenea, câți lucrători lucrează în prezent și cât este limita acestui zăcământ. Acest lucru a simplificat foarte mult interacțiunea jucătorului cu baza, permițându-i să se concentreze pe aspecte mai importante ale jocului: construirea unei baze, acumularea de trupe și distrugerea inamicului. S-ar părea că aceasta este doar o mare inovație, dar ceea ce a început pe Internet! Oamenii (cine sunt ei?) au început să țipe că jocul „a fost înșelat” și „au ucis Starcraft”. Evident, astfel de mesaje nu puteau veni decât de la „păzitorii cunoștințelor secrete” și „adepți ai APM înalte” cărora le plăcea să fie într-un club „de elită”.

Deci, revenind la subiectul nostru, cu cât trebuie să mă gândesc mai puțin la cum să scriu cod, cu atât mai mult timp am să mă gândesc la rezolvarea problemei imediate. Gândirea la ce metodă ar trebui să folosesc - PPSC sau PPZ - nu mă apropie nici un pic de rezolvarea problemei, așa că pur și simplu refuz să mă gândesc la astfel de lucruri și aleg o singură variantă: trecerea prin referință la o constantă. De ce? Pentru că nu văd niciun avantaj pentru PPP în cazuri generale, iar cazurile speciale trebuie luate în considerare separat.

Este un caz special, doar că, observând că într-o anumită metodă PPSC se dovedește a fi un blocaj, iar prin schimbarea transmisiei la PPZ, vom obține o creștere importantă a performanței, nu ezit să folosesc PPZ. Dar implicit, voi folosi PPSC atât în ​​funcțiile obișnuite, cât și în constructori. Și dacă este posibil, voi promova această metodă specială acolo unde este posibil. De ce? Pentru că cred că practica de promovare a PPP este vicioasă din cauza faptului că cea mai mare parte a programatorilor nu sunt foarte informați (fie în principiu, fie pur și simplu nu au intrat încă în leagănul lucrurilor) și pur și simplu urmează sfaturile. În plus, dacă există mai multe sfaturi contradictorii, atunci ei îl aleg pe cel mai simplu, iar acest lucru duce la apariția pesimismului în cod pur și simplu pentru că cineva a auzit ceva undeva. Oh, da, acest cineva poate oferi și un link către articolul lui Abrahams pentru a dovedi că are dreptate. Și apoi stai, citești codul și te gândești: este faptul că parametrul este trecut prin valoare aici pentru că programatorul care a scris asta a venit din Java, doar a citit o mulțime de articole „inteligente”, sau chiar este nevoie de un specificatii tehnice?

PPSC este mult mai ușor de citit: persoana cunoaște clar „forma bună” a C++ și mergem mai departe - privirea nu zăbovește. Practica folosirii PPSC a fost predată programatorilor C++ de ani de zile, care este motivul pentru care o abandonează? Acest lucru mă duce la o altă concluzie: dacă o interfață de metodă folosește un PPP, atunci ar trebui să existe și un comentariu acolo cu privire la motivul pentru care este așa. În alte cazuri, trebuie aplicat PPSC. Desigur, există tipuri de excepții, dar nu le menționez aici pur și simplu pentru că sunt implicite: string_view , initializer_list , diverse iteratoare etc. Dar acestea sunt excepții, a căror listă se poate extinde în funcție de tipurile utilizate în proiect. Dar esența rămâne aceeași de la C++98: implicit folosim întotdeauna PPCS.

Pentru std::string, cel mai probabil, nu va fi nicio diferență pe șirurile mici, despre asta vom vorbi mai târziu.

Parametrii pot fi transferați unei funcții în unul dintre următoarele moduri:

La transmiterea argumentelor după valoare, compilatorul creează o copie temporară a obiectului care urmează să fie transmis și o plasează într-o zonă a memoriei stivei desemnată pentru stocarea obiectelor locale. Funcția apelată operează pe această copie fără a afecta obiectul original. Prototipurile de funcții care preiau argumente după valoare furnizează tipul obiectului mai degrabă decât adresa sa ca parametri. De exemplu, funcția

int GetMax(int, int);

ia două argumente întregi după valoare.

Dacă este necesar ca funcția să modifice obiectul original, se folosește transmiterea parametrilor prin referință. În acest caz, nu obiectul în sine este transmis funcției, ci doar adresa acesteia. Astfel, toate modificările în corpul funcției argumentelor transmise acestuia prin referință afectează obiectul. Având în vedere că o funcție poate returna doar o singură valoare, utilizarea adresei unui obiect este o modalitate foarte eficientă de a gestiona cantități mari de date. În plus, deoarece adresa este transferată, și nu obiectul în sine, memoria stivei este salvată în mod semnificativ.

Folosind pointeri.

Sintaxa de transmitere referenţială implică utilizarea unei referinţe la tipul de obiect ca argument. De exemplu, funcția

adeziv dublu (long& var1, int& var2);

primește două referințe la variabile de tip long și int. Când trece un parametru de referință unei funcții, compilatorul transmite automat adresa variabilei specificate ca argument funcției. Nu este nevoie să plasați un ampersand înaintea unui argument într-un apel de funcție. De exemplu, pentru funcția anterioară, un apel care transmite parametrii prin referință arată astfel:

Lipici (var1, var2);

Un exemplu de prototip de funcție la trecerea parametrilor printr-un pointer este dat mai jos:

void SetNumber(int*, long*);

În plus, funcțiile pot returna nu numai valoarea unei variabile, ci și un pointer sau referință la aceasta. De exemplu, funcții al căror prototip este:

*int Count(int); &int Creștere();

returnează un pointer și, respectiv, o referință la o variabilă întreagă de tip int. Fiți conștienți de faptul că returnarea unei referințe sau a unui pointer de la o funcție poate cauza probleme dacă variabila la care se face referire nu este în domeniul de aplicare. De exemplu,

Eficiența transmiterii adresei unui obiect în locul variabilei în sine este remarcabilă și în viteza de funcționare, mai ales dacă sunt folosite obiecte mari, în special matrice (vor fi discutate mai târziu).

Dacă trebuie să treceți un obiect destul de mare unei funcții, dar modificarea acestuia nu este intenționată, în practică se folosește trecerea unui pointer constant. Acest tip de apel implică utilizarea cuvântului cheie const, de exemplu, funcție

const int* FName(int* const Number)

acceptă și returnează un pointer către un obiect constant de tip int. Orice încercare de a modifica un astfel de obiect în corpul funcției apelate va genera un mesaj de eroare de la compilator. Să ne uităm la un exemplu care ilustrează utilizarea indicatoarelor constante.

#include

int* const call(int* const);

int X = 13; int* pX = call(pX);

int* const call(int* const x)

//*x++; Nu poți modifica obiectul! întoarce x;

În loc de sintaxa indicatorului const de mai sus, puteți utiliza alternativ referințe const când transmiteți parametri, de exemplu:

const int& FName (const int& Număr)

având aceeași semnificație ca indicatoarele constante.

#include

const int& call(const int& x)

// nu poți modifica un obiect!

Deci, să fie Factorial(n) o funcție pentru calcularea factorialului unui număr n. Apoi, având în vedere că „știm” că factorialul 1 este 1, putem construi următorul lanț:

Factorial(4)=Factorial(3)*4

Factorial(3)=Factorial(2)*3

Factorial(2)=Factorial(1)*2

Dar, dacă nu am fi avut o condiție terminală ca atunci când n=1 funcția Factorial să returneze 1, atunci un astfel de lanț teoretic nu s-ar fi încheiat niciodată, iar aceasta ar fi putut fi o eroare Call Stack Overflow - call stack overflow. Pentru a înțelege ce este o stivă de apeluri și cum se poate depăși, să ne uităm la implementarea recursivă a funcției noastre:

Funcție factorială (n: Integer): LongInt;

Dacă n=1 Atunci

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

Sfârşit;

După cum vedem, pentru ca lanțul să funcționeze corect, înainte de fiecare apel de funcție următoare la sine, este necesar să salvați undeva toate variabilele locale, astfel încât atunci când lanțul este inversat, rezultatul să fie corect (valoarea calculată a factorial de n-1 este înmulțit cu n ). În cazul nostru, de fiecare dată când funcția factorială este apelată de la sine, toate valorile variabilei n trebuie salvate. Zona în care variabilele locale ale unei funcții sunt stocate atunci când se apelează recursiv se numește stiva de apeluri. Desigur, această stivă nu este infinită și poate fi epuizată dacă apelurile recursive sunt construite incorect. Finitudinea iterațiilor exemplului nostru este garantată de faptul că atunci când n=1 apelul funcției se oprește.

Transmiterea parametrilor după valoare și prin referință

Până acum nu am putut modifica valoarea din subrutină parametru real(adică, parametrul care este specificat la apelarea subrutinei), iar în unele sarcini aplicate acest lucru ar fi convenabil. Să ne amintim procedura Val, care modifică valoarea a doi dintre parametrii săi actuali simultan: primul este parametrul în care se va scrie valoarea convertită a variabilei șir, iar al doilea este parametrul Code, unde numărul caracterul este plasat în caz de eșec în timpul conversiei de tip. Aceste. există încă un mecanism prin care o subrutină poate modifica parametrii actuali. Acest lucru este posibil datorită diferitelor moduri de transmitere a parametrilor. Să aruncăm o privire mai atentă la aceste metode.

Programare in Pascal

Transmiterea parametrilor după valoare

În esență, așa am trecut toți parametrii rutinelor noastre. Mecanismul este următorul: atunci când este specificat un parametru real, valoarea acestuia este copiată în zona de memorie în care se află subrutina și apoi, după ce funcția sau procedura și-a încheiat activitatea, această zonă este șters. Aproximativ vorbind, în timp ce o subrutină rulează, există două copii ale parametrilor săi: una în domeniul de aplicare al programului apelant și a doua în domeniul de aplicare al funcției.

Cu această metodă de transmitere a parametrilor, este nevoie de mai mult timp pentru a apela subrutinei, deoarece pe lângă apelul în sine, este necesar să copiați toate valorile tuturor parametrilor actuali. Dacă o cantitate mare de date este transmisă subrutinei (de exemplu, o matrice cu un număr mare de elemente), timpul necesar pentru copierea datelor în zona locală poate fi semnificativ și acest lucru trebuie luat în considerare la dezvoltarea programelor și găsind blocaje în performanța lor.

Cu această metodă de transfer, parametrii actuali nu pot fi modificați de subrutină, deoarece modificările vor afecta doar o zonă locală izolată, care va fi eliberată după finalizarea funcției sau procedurii.

Transmiterea parametrilor prin referință

Cu această metodă, valorile parametrilor actuali nu sunt copiate în subrutină, ci sunt transferate adresele din memorie (legături către variabile) unde sunt localizați. În acest caz, subrutina modifică deja valori care nu sunt în domeniul local, astfel încât toate modificările vor fi vizibile pentru programul apelant.

Pentru a indica faptul că un argument trebuie transmis prin referință, cuvântul cheie var este adăugat înainte de declararea acestuia:

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

n1:=aleatoriu(interval);

n2:=aleatoriu(interval); Sfârşit ;

var rand1, rand2: Integer;

ÎNCEPE getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Sfârşit.

În acest exemplu, referințele la două variabile sunt transmise procedurii getTwoRandom ca parametri reali: rand1 și rand2. Al treilea parametru real (10) este transmis prin valoare. Procedura scrie folosind parametri formali