Utilizarea eficientă a vectorilor STL. Scott Meyers - Utilizarea STL în mod eficient


Multe cărți descriu capacitățile STL, dar numai aceasta vorbește despre cum să lucrezi cu această bibliotecă. Fiecare dintre cele 50 de sfaturi ale cărții este susținut de analize și exemple convingătoare, astfel încât cititorul nu va învăța doar cum să rezolve o anumită problemă, ci și când să aleagă o anumită soluție - și de ce.

„...Nu era nicio panglică pe ea! Nu a existat nicio scurtătură! Nu era nicio cutie și nici geantă!” Dr. Suess, Cum Grinch a furat Crăciunul

Prefaţă

„...Nu era nicio panglică pe ea! Nu a existat nicio scurtătură! Nu era nicio cutie și nici un sac!”

Dr. Suess, Cum Grinch a furat Crăciunul

Am scris pentru prima dată despre STL (Standard Template Library) în 1995. Cartea mea „C++ mai eficient” se termina o scurtă prezentare generală biblioteci. Dar acest lucru nu a fost suficient și în curând am început să primesc mesaje care mă întrebau când va fi scrisă cartea „STL efectiv”.

Am rezistat acestei idei de câțiva ani. La început nu aveam suficientă experiență în programarea STL și nu am considerat posibil să dau sfaturi. Dar timpul a trecut, iar această problemă a fost înlocuită cu altele. Fără îndoială, apariția bibliotecii a însemnat o descoperire în domeniul arhitecturii scalabile eficiente, dar în domeniul utilizării STL au apărut probleme pur practice asupra cărora era imposibil să închizi ochii. Adaptarea oricărui programe STL, cu excepția celor mai simple, a fost asociat cu multe probleme, ceea ce a fost explicat nu numai prin diferențele de implementare, ci și diferite niveluri Suport pentru șablon de compilator. Manualele despre STL erau rare, așa că înțelegerea „Tao al programării STL” nu a fost o sarcină ușoară. Și de îndată ce programatorul a făcut față acestei dificultăți, a apărut o alta - căutarea unei documentații de referință suficient de complete și precise. Chiar și cea mai mică eroare la utilizarea STL a fost însoțită de o avalanșă de mesaje de diagnosticare a compilatorului, a căror lungime ajungea la câteva mii de caractere și, în majoritatea cazurilor, erau despre clase, funcții și șabloane care nu erau menționate în program. Cu tot respectul pentru STL și dezvoltatorii acestei biblioteci, am ezitat să o recomand programatorilor de nivel mediu. Nu eram sigur că STL poate fi folosit eficient.

Apoi am observat ceva uimitor. În ciuda tuturor problemelor de portare și a calității proaste a documentației, în ciuda mesajelor compilatorului care semănau cu un amestec fără sens de simboluri, mulți dintre clienții mei încă lucrau cu STL. Mai mult, nu numai că au experimentat cu biblioteca, ci au folosit-o în versiuni comerciale ale programelor lor! A fost o revelație pentru mine. Știam că programele care foloseau STL au o arhitectură elegantă, dar orice bibliotecă pentru care programatorul s-a supus de bunăvoie la dificultăți de portare, documentație proastă și mesaje de eroare confuze trebuia să aibă ceva mai mult decât o arhitectură bună. Din ce în ce mai mulți programatori profesioniști credeau că chiar și o implementare STL proastă este mai bună decât nicio implementare STL.

Mai mult, știam că situația cu STL se va îmbunătăți. Bibliotecile și compilatoarele se vor apropia treptat de cerințele Standardului (așa s-a întâmplat), va apărea documentație de înaltă calitate (vezi lista de referințe la pagina 203), iar diagnosticarea compilatorului va deveni mai inteligibilă (în acest domeniu, situația lasă de dorit, dar recomandările Consiliului 49 vă vor ajuta cu decriptarea mesajelor). Așa că, am decis să contribui la mișcarea STL. Așa a apărut această carte - 50 sfaturi practice despre utilizarea STL în C++.

La început am intenționat să scriu o carte pentru a doua jumătate a anului 1999 și chiar am schițat structura ei grosieră. Dar apoi planurile s-au schimbat, am întrerupt munca la carte și am dezvoltat un curs introductiv despre STL, care a fost predat mai multor grupuri de programatori. Aproximativ un an mai târziu, m-am întors la carte și am extins foarte mult materialul pe baza experiențelor pe care le-am câștigat în timp ce predam. În carte, am încercat să acopăr aspectele practice ale programării în STL, deosebit de importante pentru programatorii profesioniști.

Scott Douglas Meyers Stafford, Oregon aprilie 2001

Prefaţă

„...Nu era nicio panglică pe ea! Nu a existat nicio scurtătură! Nu era nicio cutie și nici un sac!”

Dr. Suess, Cum Grinch a furat Crăciunul

Am scris pentru prima dată despre STL (Standard Template Library) în 1995. Cartea mea „More Effective C++” s-a încheiat cu o scurtă prezentare generală a bibliotecii. Dar acest lucru nu a fost suficient și în curând am început să primesc mesaje care mă întrebau când va fi scrisă cartea „STL efectiv”.

Am rezistat acestei idei de câțiva ani. La început nu aveam suficientă experiență în programarea STL și nu am considerat posibil să dau sfaturi. Dar timpul a trecut, iar această problemă a fost înlocuită cu altele. Fără îndoială, apariția bibliotecii a însemnat o descoperire în domeniul arhitecturii scalabile eficiente, dar în domeniu utilizare STL a întâmpinat probleme pur practice la care era imposibil să închizi ochii. Adaptarea tuturor programelor STL, cu excepția celor mai simple, a fost plină de multe probleme, datorate nu numai diferențelor de implementare, ci și diferitelor niveluri de suport pentru șablonul compilatorului. Manualele despre STL erau rare, așa că înțelegerea „Tao al programării STL” nu a fost o sarcină ușoară. Și de îndată ce programatorul a făcut față acestei dificultăți, a apărut o alta - căutarea unei documentații de referință suficient de complete și precise. Chiar și cea mai mică eroare la utilizarea STL a fost însoțită de o avalanșă de mesaje de diagnosticare a compilatorului, a căror lungime ajungea la câteva mii de caractere și, în majoritatea cazurilor, erau despre clase, funcții și șabloane care nu erau menționate în program. Cu tot respectul pentru STL și dezvoltatorii acestei biblioteci, am ezitat să o recomand programatorilor de nivel mediu. Nu eram sigur de STL Poate sa utilizați eficient.

Apoi am observat ceva uimitor. În ciuda tuturor problemelor de portare și a calității proaste a documentației, în ciuda mesajelor compilatorului care semănau cu un amestec fără sens de simboluri, mulți dintre clienții mei încă au lucrat cu STL. Mai mult, nu numai că au experimentat cu biblioteca, ci au folosit-o în versiuni comerciale ale programelor lor! Pentru mine asta

a fost o revelație. Știam că programele care foloseau STL au o arhitectură elegantă, dar orice bibliotecă pentru care programatorul s-a supus de bunăvoie la dificultăți de portare, documentație proastă și mesaje de eroare confuze trebuia să aibă ceva mai mult decât o arhitectură bună. Din ce în ce mai mulți programatori profesioniști credeau că chiar și o implementare STL proastă este mai bună decât nicio implementare STL.

Mai mult, știam că situația cu STL se va îmbunătăți. Bibliotecile și compilatoarele se vor apropia treptat de cerințele Standardului (așa s-a întâmplat), va apărea documentație de înaltă calitate (vezi lista de referințe la pagina 203), iar diagnosticarea compilatorului va deveni mai inteligibilă (în acest domeniu, situația lasă de dorit, dar recomandările Consiliului 49 vă vor ajuta cu decriptarea mesajelor). Așa că, am decis să contribui la mișcarea STL. Așa a apărut această carte - 50 de sfaturi practice despre utilizarea STL în C++.

La început am intenționat să scriu o carte pentru a doua jumătate a anului 1999 și chiar am schițat structura ei grosieră. Dar apoi planurile s-au schimbat, am întrerupt munca la carte și am dezvoltat un curs introductiv despre STL, care a fost predat mai multor grupuri de programatori. Aproximativ un an mai târziu, m-am întors la carte și am extins foarte mult materialul pe baza experiențelor pe care le-am câștigat în timp ce predam. În carte, am încercat să acopăr aspectele practice ale programării în STL, deosebit de importante pentru programatorii profesioniști.

Scott Douglas Meyers Stafford, Oregon, aprilie 2001

Din cartea C++ de Hill Murray

Prefață Limbajul modelează modul în care gândim și determină la ce putem gândi. B.L. Worf C++ este limbă universală programare, concepută pentru a face programarea mai plăcută pentru programatorul serios. Cu excepția celor minori

Din carte Centrul de muzică pe computer autor Leontiev Vitali Petrovici

Prefață Cu greu cineva își poate imagina un computer modern fără sunet. Dar la început a fost așa. Computerele au fost create pentru calcularea serioasă în organizații speciale, singurele sunete ale cărora erau zgomotul ventilatoarelor și zgomotul imprimantelor. CU

Din carte Microsoft Office autor Leontiev Vitali Petrovici

Prefață Nu există nicio îndoială că așa-numitul programe de birou– Cele mai populare și cele mai multe Programe utile din tot ce poate trăi în burta de fier a computerului tău. Și dacă știți deja cum să porniți un computer, să instalați programe, să lucrați cu

Din cartea Procese ciclu de viață software autor autor necunoscut

Din cartea TEHNOLOGIA INFORMAȚIEI. GHID DE GESTIONARE A DOCUMENTAȚIEI SOFTWARE autor autor necunoscut

Prefaţă 1. DEZVOLTATĂ ŞI INTRODUSĂ de Comitetul Tehnic de Standardizare TC 22 „Tehnologia Informaţiei”2. APROBAT ȘI INTRAT ÎN VIGOARE prin Rezoluția Standardului de Stat al Rusiei din 20 decembrie 1993 Nr. 260 Standardul a fost întocmit pe baza utilizării textului autentic al tehnicilor tehnice.

Din carte Factorul umanîn programare autor Konstantin Larry L

Prefață Cealaltă Parte software Această carte este despre cealaltă parte a software-ului - cea care analizează lumea exterioară. Această parte a computerelor este despre oameni - tehnicieni ca tine și mine și oameni normali ca tine si mine. Notele colectate aici explorează

Din cartea 300 cele mai bune programe pentru toate ocaziile autor Leontiev Vitali Petrovici

Prefață Toate cele mai necesare lucruri din această lume au o proprietate cea mai neplăcută: nu sunt niciodată la îndemână la momentul potrivit. Știința nu știe dacă aceasta este o consecință a notoriei „legi sandwich” sau a abstinenței umane elementare. Rezultatul este totul

Din cartea BPwin și Erwin. Instrumente de dezvoltare CASE sisteme de informare autor

Prefaţă Crearea sistemelor informaţionale moderne este o sarcină descurajantă, a cărui soluție necesită utilizarea unor tehnici și instrumente speciale. Nu este de mirare că în În ultima vreme a crescut semnificativ în rândul analiștilor și dezvoltatorilor de sisteme

Din cartea Business Process Modeling with BPwin 4.0 autor Maklakov Serghei Vladimirovici

Prefață În 1998, a fost publicată cartea autorului despre instrumentele de analiză a sistemelor și proiectarea sistemelor informaționale - BPwin și ERwin. (Maklakov S. BPwin și ERwin. CASE-instrumente pentru dezvoltarea sistemelor informaționale. M: Dialog-MEPhI). Cartea a trecut prin două ediții și

Din cartea XSLT Technology autor Valikov Alexey Nikolaevici

Prefață Despre ce este această carte? Este dificil de supraestimat impactul pe care l-a avut în ultimii doi ani tehnologia de informație apariţia şi răspândirea unui limbaj extensibil Marcaj XML(din engleză extensibil Limbajul de marcare). Tehnologiile XML și-au găsit aplicație în multe domenii și

Din cartea Tehnici de creare a interioarelor de diverse stiluri autorul Timofeev S. M.

Prefață 3ds Max - foarte program popular pentru realizarea proiectelor de interior. Programul oferă o mulțime de oportunități pentru a crea o imagine fotorealistă a viitorului interior, vă permite să transmiteți mai multe concepte de design pentru aceeași cameră,

Din cartea 19 păcate capitale care amenință securitatea software-ului de Howard Michael

Prefață Teoria calculatoarelor se bazează pe presupunerea că mașinile se comportă determinist. De obicei, ne așteptăm ca un computer să se comporte așa cum l-am programat. De fapt, aceasta este doar o presupunere aproximativă. Calculatoare moderne general

Din cartea Cum să hrănești un elefant sau primii pași către auto-organizare cu Evernote de Sultanov Gani

Prefață Cartea este dedicată sistemului de gestionare a afacerilor și de colectare a informațiilor folosind serviciul Evernote Iată ce este scris despre această mini-carte pe blogul oficial Evernote: „Cartea va fi deosebit de interesantă pentru cei care se uită de mult. Metoda GTD de creștere a eficienței personale

Din cartea Introducere în criptografie autor Zimmermann Filip

Prefață Criptografia este o temă comună în benzile desenate pentru copii și povești de spionaj. Copiii au strâns odată etichete Ovaltine® pentru a obține Inelul de decodor secret al Căpitanului Midnight. Aproape toată lumea a vizionat un film de televiziune despre un domn discret îmbrăcat într-un costum cu

Din cartea iOS. Tehnici de programare autor Nahavandipur Vandad

Prefață Această ediție a cărții nu este doar o versiune extinsă, ci o versiune complet revizuită a celei anterioare. Totul s-a schimbat în iOS 7: aspectși partea funcțională sistem de operare, modalități de a folosi dispozitivele noastre iOS și, cel mai important, principii

Din cartea Programatorul fanatic de Chad Fowler

Prefață Sunt sigur că există ceva extraordinar în fiecare dintre noi, dar se petrece mult timp pentru a înțelege ce este cu adevărat important, încercând să-l scoatem din noi înșine. Nu poți deveni extraordinar dacă nu îți iubești mediul, uneltele, domeniul tău.

Utilizarea eficientă a STL și a șabloanelor

Serghei Satsky

Introducere

Cu ajutorul mașinilor cu stări finite (denumite în continuare pur și simplu automate), puteți rezolva cu succes o clasă largă de probleme. Această împrejurare s-a remarcat de multă vreme, prin urmare, în literatura de specialitate despre proiectarea software-ului, se dau adesea discuții pe tema folosirii automatelor (, ,). Cu toate acestea, în timpul procesului de simulare, mașina este privită dintr-o mai mare nivel inalt decât aceasta se face în momentul implementării sale folosind limbaj specific programare.

Ultima dată când Jurnalul utilizatorului C/C++ a abordat problema designului mașinii cu stări finite a fost în numărul din mai 2000 (). Acest număr a prezentat un articol al lui David Lafreniere, în care autorul a descris abordarea pe care a folosit-o. A trecut mult timp de atunci și acest articol va încerca să adopte o abordare diferită a designului mașină cu stări finite luând în considerare tendințe moderneîn proiectarea software-ului.

Pentru comoditate, luați în considerare un exemplu simplu care va fi folosit mai jos. Să presupunem că trebuie să determinați dacă o secvență de caractere de intrare este un număr întreg, un identificator valid sau un șir nevalid. Prin identificatori validi înțelegem acei identificatori care încep cu o literă și apoi conțin litere și „/” sau cifre. Mașina care va ajuta la rezolvarea problemei este prezentată în Figura 1. Figura folosește notația UML (Unified Modeling Language).

Figura 1. Un automat care vă permite să aflați care este șirul de intrare.

Trebuie remarcat faptul că diverse surseînzestrați o astfel de mașină cu diverse atribute. Campionul în ceea ce privește numărul lor este probabil UML (). Aici puteți găsi evenimente amânate, tranziții interne, declanșatoare de evenimente cu parametri, conditii suplimentare tranziții (condiții de pază), funcții de intrare (acțiuni de intrare), funcții de ieșire (acțiuni de ieșire), evenimente de cronometru etc. Totuși, aici, pentru simplitatea prezentării, vom omite atribute suplimentare (vom reveni la unele dintre ele mai târziu) și ne vom concentra pe un model simplu în care există stări, evenimente și tranziții între stări.

Pe acest moment tot ce avem este un model vizual și ușor de realizat. Cum putem trece acum de la el la codul C++? Cel mai simplu mod de a-l implementa este un set de instrucțiuni if ​​într-o formă sau alta. De exemplu:

comutator(CurrentState)

caz State1: if (CurrentEvent == Event1)

dacă (CurrentEvent == Event2)

caz State2:... .

Nu prea grafic, nu? Acum să ne imaginăm că avem zeci de evenimente și zeci de state. Acest tip de cod este greu de vizualizat. Probleme deosebit de grave apar la întreținerea codului, când trebuie să reveniți la acesta după câteva luni și să faceți corecții.

O altă implementare posibilă este un set de funcții, fiecare dintre acestea reprezentând o stare. O astfel de funcție va putea returna un pointer către funcția care corespunde noii stări a mașinii. Această implementare nu face codul mai ușor de întreținut.

Să abordăm problema dintr-un unghi ușor diferit. Poza este bună, dar în forma sa originală nu poate fi în niciun fel transferată text original. Aceasta înseamnă că, chiar dacă se găsește o soluție, va fi ceva intermediar între o imagine și un text obișnuit.

Abordarea implementării mașinii

O astfel de reprezentare intermediară a mașinii poate fi un tabel. Această metodă este cunoscută de mult timp, de exemplu. Să creăm un tabel de tranziție pentru mașina noastră (Tabelul 1).

Tabelul 1.

Aici primul rând listează stările posibile, iar prima coloană listează evenimentele posibile. La intersecții sunt indicate stările la care trebuie să aibă loc tranziția.

Reprezentarea unui automat sub formă de tabel este mult mai clară decât reprezentarea „untată” a aceluiași automat sub forma declarații condiționale sau funcții de tranziție. Acum puteți încerca să traduceți tabelul în cod.

Să presupunem că am reușit să traducem tabelul în cod. Cum ai vrea să arate acest cod? Să formulăm cerințele pentru aceasta ():

Descrierea mașinii (citiți tabelul) ar trebui să fie concentrată într-un singur loc. Acest lucru va face aparatul ușor de citit, înțeles și modificat.

Reprezentarea automatului trebuie să fie de tip safe.

Nu ar trebui să existe restricții privind numărul de state și evenimente.

Am dori să reprezentăm evenimentele și stările ca tipuri abstracte definite de utilizator.

Dacă este posibil, aș dori să fac mașina flexibilă și ușor de extins.

Dacă este posibil, aș dori să verific descrierea mașinii.

Dacă este posibil, dorim să excludem utilizarea incorectă a mașinii.

Cerințele 1 și 7 înseamnă că ar fi bine să plasați întreaga descriere a mașinii în constructor. În constructor, este necesar să se verifice corectitudinea descrierii - cerința 6.

Cerința 2 înseamnă că nu trebuie utilizate operațiuni nesigure precum reinterpret_cast.

Vom vorbi despre cerința 5 mai târziu, dar acum să discutăm despre cerința 3. B caz general numărul de stări posibile (adică numărul de coloane din tabel) este necunoscut. Numărul de evenimente (adică numărul de rânduri din tabel) este, de asemenea, necunoscut. Se pare că constructorul clasei, care va fi un automat, are un număr variabil de argumente. La prima vedere, această problemă pare ușor de rezolvat folosind funcțiile limbajului C va_arg(), va_copy(), va_end() și va_start()(). Cu toate acestea, nu toate sunt atât de simple. Pentru aceste funcții este necesar să se furnizeze semne pentru sfârșitul listelor, iar în cazul nostru numărul de elemente din rânduri și coloane este necunoscut. Nu este indicat să specificați dimensiunea. În plus, aceste funcții sunt garantate să funcționeze numai pentru POD (Plain Old Data) și pentru tipuri arbitrare necazurile sunt posibile.

Să ne apropiem din partea cealaltă. Să scriem cum am dori să vedem constructorul mașinii:

La apelarea constructorului în acest fel, prin formatarea textului într-un font monospațiu, descrierii mașinii i se poate da aspectul unui tabel. Să ne imaginăm:

SFiniteStateMachine A(

„gol”, „număr”, „identificator”, „necunoscut”,

litera, „identificator”, „necunoscut”, „identificator”, „necunoscut”,

cifră, „număr”, „număr”, „identificator”, „necunoscut”

Starea de început este simplă: este doar un obiect al unei clase care reprezintă starea. Cu o listă de state, și cu atât mai mult cu o listă de tranziții, lucrurile sunt mai complicate. Nu este posibil să enumerați stările separate prin virgule. Mai mult, ar fi convenabil ca SFiniteStateMachine să aibă un număr fix de argumente. Se dovedește că acest lucru este posibil. La urma urmei, putem crea obiecte temporare, fiecare dintre ele se va ocupa de propria listă.

Să ne uităm la lista statelor. Aceeași problemă rămâne aici - un număr nedefinit de state. Supraîncărcarea operatorului și constructorul implicit pot ajuta la rezolvarea acestei probleme. Tot nu ar fi posibil să enumerați argumentele separate prin virgulă, dar un alt separator ar fi potrivit în loc de virgulă. Un astfel de separator ar putea fi<<, то есть обработку списка состояний можно записать так:

Să facem același lucru cu lista de tranziții pentru un eveniment. Singura diferență va fi că fiecare listă de tranziții are încă un atribut - un eveniment pentru care sunt descrise tranzițiile. Constructorul STransitionsProxy va lua un argument: eveniment și operatorul supraîncărcat<< будет принимать состояния.

Operator supraîncărcat<< проверит, что сначала идет список состояний, что список состояний только один, что в списках переходов нет повторяющихся событий и в переходах указаны только состояния, указанные в списке состояний. operator<< также проверит, что количество состояний в списках переходов равно количеству состояний в списке состояний. В результате конструктор SFiniteStateMachine будет выглядеть так:

Constructorul SFiniteStateMachine va fi însărcinat cu verificarea stării inițiale. Ar trebui să fie în lista statelor.

Prin formatarea textului, am reușit deja să dăm argumentelor constructorului aspectul unui tabel. Cu toate acestea, asta nu este tot. La descrierea mașinii, toate detaliile legate de șabloane au fost omise. În practică, aceasta înseamnă că în timpul construcției va trebui să specificați și tipurile, care vor „împrăștia” în continuare textul. În ciuda problemelor asociate cu preprocesorul, acesta va ajuta aici. Argumentele constructorului vor arăta cam așa:

FSMBEGIN(„gol”)

FSMSTATES „gol”<< “number” << “identifier” << “unknown”

FSMEVENT(litera) „identificator”<< “unknown” << “identifier” << “unknown”

FSMEVENT(cifră) „număr”<< “number” << “identifier” << “unknown”

Această înregistrare este deja acceptabilă pentru utilizarea de zi cu zi.

Detalii de implementare

Implementarea trebuie să includă o serie de elemente de sprijin, în special excepții. Mașina le va emite în cazul unei erori în descrierea stărilor și tranzițiilor. Când vă dezvoltați propria clasă de excepție, puteți utiliza moștenirea din clasa de excepție standard. Acest lucru va face posibilă specificarea doar a unei referințe la clasa de excepție standard subiacentă în blocul catch. Puteți defini clasa de excepție astfel:

Să revenim la designeri. Deoarece se ocupă de liste de lungime variabilă, este logic să folosiți containerele furnizate de biblioteca STL() pentru a stoca elemente. Pentru a stoca o listă unidimensională, vom folosi un container de vectori, iar pentru un tabel de tranziție, un vector de vectori:

Deoarece containerul vectorial acceptă operatorul , puteți utiliza o construcție similară în tabelul de tranziție pentru a găsi starea la care doriți să treceți:

Desigur, clasa de automate va trebui să aibă o funcție care să primească și să proceseze evenimentul. Există două opțiuni. Prima este o funcție, a doua este o supraîncărcare a unui operator. Pentru a oferi flexibilitate suplimentară, implementăm ambele opțiuni:

Rămâne întrebarea: ce să faci dacă vine un eveniment pentru care mașina nu are o descriere de tranziție? Opțiunile sunt pur și simplu să ignorați un astfel de eveniment, să aruncați o excepție sau să faceți ceva definit de utilizator. Să folosim ideea de strategii () și să includem printre argumentele șablonului un functor care va determina strategia comportamentală dorită. Această abordare este pe deplin în concordanță cu cerința 5. În acest caz, puteți seta o strategie implicită - de exemplu, să aruncați o excepție. Acum antetul șablonului arată astfel:

Dacă aveți nevoie de alte acțiuni, puteți oricând să scrieți propriul functor în imaginea și asemănarea cu SIgnoreStrategy și să-l transmiteți șablonului.

Multe surse care descriu mașinile cu stări finite menționează capacitatea de a apela funcții la intrarea și ieșirea dintr-o stare. Această capacitate poate fi furnizată cu ușurință folosind aceeași abordare strategică. Este convenabil să definiți funcțiile de intrare și ieșire a stării pentru o clasă care reprezintă o anumită stare. Amintindu-ne de cerința 5, să oferim o gestionare flexibilă a acestei caracteristici. Presupunând că funcțiile clasei de stare vor fi numite OnEnter și OnExit, putem scrie mai mulți functori gata făcut: unul care nu apelează nicio funcție, unul care apelează doar OnEnter, unul care apelează doar OnExit și unul care apelează ambele funcții.

șablon

clasa SEmptyFunctor

(întoarcere;)

șablon

clasa SOnEnterFunctor

operator void inline() (SState & From, const SEvent & Event, SState & To)

( Către.OnEnter(De la, Eveniment); )

șablon

clasa SOnExitFunctor

operator void inline() (SState & From, const SEvent & Event, SState & To)

( From.OnExit(Event, To); )

șablon

clasa SOnMoveFunctor

operator void inline() (SState & From, const SEvent & Event, SState & To)

( From.OnExit(Eveniment, Către); To.OnEnter(From, Event); )

Strategia implicită (nu apelează nicio funcție) poate fi transmisă ca argument șablon. Strategia de apelare a funcțiilor este probabil să se schimbe mai des decât strategia de a face față unui eveniment necunoscut. Prin urmare, este logic să îl plasați în lista de argumente înainte de strategia de reacție la un eveniment necunoscut:

șablon

clasa SFunctor = SEmptyFunctor ,

clasa SUnknownEventStrategy = SThrowStrategy >

clasa SFiniteStateMachine (... );

O altă problemă cu evenimentele este că un eveniment poate fi generat în interiorul unei funcții numite atunci când o stare este ieșită sau intră. Pentru a gestiona astfel de evenimente, trebuie să proiectați funcția care primește evenimentul în consecință. Ținând cont de astfel de evenimente „interne”, este necesar să se prevadă o coadă în care vor fi plasate evenimentele. Codul care gestionează tranzițiile va trebui să facă acest lucru până când coada este goală. Vom folosi deque de la STL ca container potrivit pentru stocarea evenimentelor. Deoarece trebuie să introducem elemente doar la început și să excludem de la sfârșitul containerului, fără acces aleatoriu, containerul deque este cel mai potrivit.

A mai rămas foarte puțin. Uneori trebuie să readuceți mașina la starea inițială. Ca și în cazul evenimentelor, oferim două opțiuni: o funcție obișnuită și un operator supraîncărcat<<. Для перегруженного operator << нужно определить специальный манипулятор:

Rezultatul funcționării mașinii este starea în care a trecut. Pentru a obține starea curentă, vom scrie o funcție și vom supraîncărca operatorul de ieșire în fluxul clasei automate:

După cum sa menționat deja, mai multe macrocomenzi sunt definite pentru a reduce timpul de tastare și lizibilitatea. Acestea necesită înlocuiri predefinite pentru tipurile de evenimente și stări. Cerința se datorează faptului că utilizarea directivelor de preprocesor imbricate este imposibilă. Șablonul folosește clase Proxy, care au nevoie și de informații despre tip. Prin urmare, pentru a utiliza macrocomenzi, va trebui să faceți acest lucru:

#define FSMStateType șir // Tip de stare

#define FSMEventType int // Tipul evenimentului

#undef FSMStateType

#undef FSMEventType

Există o alternativă: specificați complet toate tipurile.

Tot ce rămâne este să plasați șablonul în spațiul de nume. După aceea, îl poți folosi.

Exemplu de utilizare a șablonului

Să scriem cod pentru a rezolva problema pusă la începutul articolului.

#include

#include

folosind namespace std;

#include „FiniteStateMachine.h”

utilizarea spațiului de nume FSM;

// Definiți tipul pentru evenimente

enum Evenimente (litera = 0, cifra = 1);

int main(int argc, char ** argv)

#define șirul FSMStateType

#define Evenimente FSMEventType

SFiniteStateMachine< StateType,

SEmptyFunctor ,

SThrowStrategy

FSMBEGIN(„gol”)

FSMSTATES „gol”<< «number» << «identifier» << «unknown»

FSMEVENT(litera) „identificator”<< «unknown» << «identifier» << «unknown»

FSMEVENT(cifră) „număr”<< «number» << «identifier» << «unknown»

#undef FSMStateType

#undef FSMEventType

cout<< «StartState is: » << MyMachine << endl;

MyMachine<< digit << digit << letter;

cout<< «The "unknown" state is expected. Current state is: » << MyMachine << endl;

// Notă: sunt necesare paranteze pe rândul următor. Ei vor oferi

// ordinea corectă de execuție a instrucțiunilor

cout<< «Reset the machine. Current state is: » << (MyMachine << ResetMachine) << endl;

MyMachine<< letter << digit << letter;

cout<< «The "identifier" state is expected. Current state is: » << MyMachine << endl;

Exemplul omite în mod deliberat detalii precum gestionarea excepțiilor și introducerea de funcții care sunt apelate la intrarea și ieșirea dintr-o stare. Pentru a demonstra capacitatea de a defini strategiile utilizatorului, constructorul MyMachine specifică toți parametrii, inclusiv parametrii impliciti.

Cerințe pentru aplicațiile client

Cerințele sunt puține. Clasele de evenimente și stări trebuie să aibă operator==, operator= și un constructor de copiere definit. operator== este folosit pentru a căuta evenimente și stări în liste folosind algoritmul de căutare STL. operator= este folosit la copierea elementelor listei. Constructorul de copiere este folosit la inițializarea listelor și a altor elemente.

Dacă clientul folosește functorul furnizat pentru a apela funcțiile de intrare și ieșire, atunci clasa de stare trebuie să implementeze funcțiile corespunzătoare: OnExit și OnEnter.

Avantajele și dezavantajele soluției propuse

Avantaje:

Șablonul este tastat puternic. Aceasta înseamnă că codul scris incorect nu va fi acceptat de compilator, iar eroarea nu va ajunge la timpul de execuție al programului.

Conceptele de stat și eveniment au fost extinse. Acum acestea sunt clase arbitrare scrise de utilizator.

Operatorul reinterpret_cast nu este utilizat<…>, ceea ce poate duce la rezultate incorecte.

Întreaga descriere a mașinii este concentrată într-un singur loc. Nu există nicio legătură cu succesiunea descrierilor reacțiilor la evenimente.

Flexibilitatea comportamentului este determinată de functorii definiți de utilizator. Este furnizat un set de functori gata pregătiți.

Este posibil să se creeze dinamic o descriere a unei mașini cu stări finite. De exemplu, puteți crea instanțe ale claselor Proxy, puteți citi descrierea mașinii dintr-un fișier și apoi creați o instanță SFiniteStateMachine.

Nu există operații pentru crearea și ștergerea obiectelor folosind operatorii new și delete.

Nu există cerințe pentru clasele de stări și evenimente (altele decât capacitatea de a le compara).

Defecte:

Există o mulțime de operațiuni de copiere la crearea unei mașini. Cu toate acestea, acest dezavantaj este parțial compensat de faptul că, de obicei, mașina este creată o dată și utilizată de mai multe ori.

Trebuie să scrieți două directive de preprocesor sau să utilizați un prefix lung. Cu toate acestea, aceasta este doar o problemă de tastare.

Personal, sunt dispus să suport această scurtă listă de dezavantaje de dragul beneficiilor obținute.

Modalități posibile de îmbunătățire a șablonului

Cititorul priceput va observa că puteți crește flexibilitatea și performanța șablonului. Următoarea listă evidențiază îmbunătățirile care sunt la suprafață:

Puteți renunța la clasa intermediară SFiniteStateMachineProxy. Acest lucru va economisi costurile de copiere, dar va introduce potențialul de utilizare incorectă a șablonului.

Puteți introduce manipulatoare care vă vor permite să indicați în mod explicit atunci când descrieți tranzițiile pe acelea care ar trebui ignorate sau să generați o excepție atunci când apar.

Siguranța firului

Șablonul folosește containere STL, care pot cauza probleme într-un mediu cu mai multe fire. Deoarece la proiectarea șablonului scopul a fost de a dezvolta o soluție independentă de platformă, nu există instrumente de sincronizare în șablon. Prezența instrumentelor de sincronizare, așa cum se știe, în funcție de situație, poate fi atât un avantaj, cât și un dezavantaj. Dacă nu sunt necesare, prezența lor va crea doar cheltuieli suplimentare. Adăugarea instrumentelor de sincronizare la un șablon nu va fi dificilă pentru un dezvoltator experimentat.

Bibliografie

Jurnalul utilizatorului C/C++, mai 2000

Booch G., Rumbaugh J., Jacobson I. Ghidul utilizatorului Unified Modeling Language. Addison-Wesley, 2001

Meyers S. STL efectiv. Addison-Wesley, 2001

Alexandrescu A. Modern C++ Design. Addison-Wesley, 2002

Lewis P., Rosenkrantz D., Stearns R. Compiler Design Theory. Addison-Wesley, 1976

Schildt H. C/C++ Referință pentru programator. A doua editie. Williams, 2000

Meyers S. C++ eficient. A doua editie. Addison-Wesley, 1998 și C++ mai eficient. Addison-Wesley, 1996

Sutter G. C++ excepțional. Addison-Wesley, 2002

Utilizarea eficientă a STL

Prefaţă

„...Nu era nicio panglică pe ea! Nu a existat nicio scurtătură! Nu era nicio cutie și nici un sac!”

Dr. Suess, Cum Grinch a furat Crăciunul

Am scris pentru prima dată despre STL (Standard Template Library) în 1995. Cartea mea „More Effective C++” s-a încheiat cu o scurtă prezentare generală a bibliotecii. Dar acest lucru nu a fost suficient și în curând am început să primesc mesaje care mă întrebau când va fi scrisă cartea „STL efectiv”.

Am rezistat acestei idei de câțiva ani. La început nu aveam suficientă experiență în programarea STL și nu am considerat posibil să dau sfaturi. Dar timpul a trecut, iar această problemă a fost înlocuită cu altele. Fără îndoială, apariția bibliotecii a însemnat o descoperire în domeniul arhitecturii scalabile eficiente, dar în domeniu utilizare STL a întâmpinat probleme pur practice la care era imposibil să închizi ochii. Adaptarea tuturor programelor STL, cu excepția celor mai simple, a fost plină de multe probleme, datorate nu numai diferențelor de implementare, ci și diferitelor niveluri de suport pentru șablonul compilatorului. Manualele despre STL erau rare, așa că înțelegerea „Tao al programării STL” nu a fost o sarcină ușoară. Și de îndată ce programatorul a făcut față acestei dificultăți, a apărut o alta - căutarea unei documentații de referință suficient de complete și precise. Chiar și cea mai mică eroare la utilizarea STL a fost însoțită de o avalanșă de mesaje de diagnosticare a compilatorului, a căror lungime ajungea la câteva mii de caractere și, în majoritatea cazurilor, erau despre clase, funcții și șabloane care nu erau menționate în program. Cu tot respectul pentru STL și dezvoltatorii acestei biblioteci, am ezitat să o recomand programatorilor de nivel mediu. Nu eram sigur de STL Poate sa utilizați eficient.

Apoi am observat ceva uimitor. În ciuda tuturor problemelor de portare și a calității proaste a documentației, în ciuda mesajelor compilatorului care semănau cu un amestec fără sens de simboluri, mulți dintre clienții mei încă lucrau cu STL. Mai mult, nu numai că au experimentat cu biblioteca, ci au folosit-o în versiuni comerciale ale programelor lor! A fost o revelație pentru mine. Știam că programele care foloseau STL au o arhitectură elegantă, dar orice bibliotecă pentru care programatorul s-a supus de bunăvoie la dificultăți de portare, documentație proastă și mesaje de eroare confuze trebuia să aibă ceva mai mult decât o arhitectură bună. Din ce în ce mai mulți programatori profesioniști credeau că chiar și o implementare STL proastă este mai bună decât nicio implementare STL.

Mai mult, știam că situația cu STL se va îmbunătăți. Bibliotecile și compilatoarele se vor apropia treptat de cerințele Standardului (așa s-a întâmplat), va apărea documentație de înaltă calitate (vezi lista de referințe la pagina 203), iar diagnosticarea compilatorului va deveni mai inteligibilă (în acest domeniu, situația lasă de dorit, dar recomandările Consiliului 49 vă vor ajuta cu decriptarea mesajelor). Așa că, am decis să contribui la mișcarea STL. Așa a apărut această carte - 50 de sfaturi practice despre utilizarea STL în C++.

La început am intenționat să scriu o carte pentru a doua jumătate a anului 1999 și chiar am schițat structura ei grosieră. Dar apoi planurile s-au schimbat, am întrerupt munca la carte și am dezvoltat un curs introductiv despre STL, care a fost predat mai multor grupuri de programatori. Aproximativ un an mai târziu, m-am întors la carte și am extins foarte mult materialul pe baza experiențelor pe care le-am câștigat în timp ce predam. În carte, am încercat să acopăr aspectele practice ale programării în STL, deosebit de importante pentru programatorii profesioniști.

Scott Douglas Meyers Stafford, Oregon, aprilie 2001

Mulțumiri

De-a lungul anilor, mi-a trebuit să înțeleg STL, să dezvolt cursul și să scriu această carte, am primit ajutor și sprijin neprețuit de la mulți oameni. Aș dori să-i mulțumesc în mod special lui Mark Rodgers, care sa oferit cu generozitate să revizuiască materialele de curs așa cum au fost scrise. Am aflat mai multe despre STL de la el decât de la oricine altcineva. Mark a servit, de asemenea, ca editor tehnic pentru această carte, iar comentariile și completările sale au contribuit la îmbunătățirea mult din material.

O altă sursă proeminentă de informații au fost forumurile Usenet; dedicat limbajului C++, în special comp.lang.c++.moderated (“clcm”), comp.std.c++ și microsoft.public.vc.stl. Timp de peste zece ani, participanții la aceste conferințe și la alte conferințe mi-au răspuns întrebărilor și au pus probleme la care a trebuit să mă gândesc. Sunt profund recunoscător comunității Usenet pentru asistența acordată în dezvoltarea acestei cărți și a publicațiilor mele anterioare C++.

Înțelegerea mea despre STL a fost modelată de o serie de publicații, dintre care cele mai importante sunt enumerate la sfârșitul cărții. Am învățat în special o mulțime de informații utile din lucrarea lui Josattis „The C++ Standard Library”.

Ideile și observațiile care compun această carte sunt în principal cele ale altor autori, deși conține o serie de propriile mele descoperiri. Am încercat cât am putut să urmăresc sursele din care a fost extras materialul, dar această sarcină este sortită eșecului, deoarece informațiile au fost colectate din mai multe surse pe o perioadă lungă de timp. Lista de mai jos este departe de a fi completă, dar nu pot oferi nimic mai bun. Vă rugăm să rețineți că această listă se referă la sursele din care am aflat despre anumite idei și tehnici, nu la inițiatorii acestora.

În punctul 1, nota că containerele de noduri oferă un suport mai bun pentru semantica tranzacțională provine din Secțiunea 5.11.2 din Biblioteca standard C++. Exemplul de utilizare a typedef la schimbarea tipului de alocare de memorie din sfatul 2 a fost sugerat de Mark Rogers. Sfatul 5 este inspirat din articolul lui Reeves „STL Gotchas”. Sfatul 8 se bazează pe sfatul 37 din cartea lui Sutter Exceptional C++, iar Kevlin Henney a oferit informații importante despre problemele întâlnite la utilizarea containerelor auto_ptr . Pe Usenet, Matt Austem a oferit exemple de utilizare a alocătorilor de memorie, pe care le-am inclus în Sfatul 11. Sfatul 12 se bazează pe material de pe site-ul SGI STL despre siguranța firelor. Informațiile despre numărarea referințelor într-un mediu cu mai multe fire din sfatul 13 provin din articolul lui Sutter. Ideea pentru sfatul 15 a fost inspirată de articolul lui Reeves „Using Standard string in the Real World, Part 2”. Metodologie de înregistrare directă a datelor în vecto r, demonstrat în sfatul 16 a fost sugerat de Mark Rogers. Sfatul 17 a inclus informații de la Usenet și a fost scris de Siemel Naran și Carl Barron. Sfatul 18 a fost preluat din articolul lui Sutter „Când un container nu este un container?” . Pentru sfatul 20, Mark Rogers a propus ideea de a converti un pointer la un obiect prin dereference, iar Scott Lewandowski a dezvoltat versiunea DereferenceLess prezentată. Sfatul 21 se bazează pe postarea lui Doug Harrison pe microsoft.public.vc.stl, dar decizia de a limita domeniul de aplicare a acestui sfat la problema egalității a fost a mea. Sfatul 22 se bazează pe articolul lui Sutter „Standard Library News: sets and maps”. Sfatul 23 a fost sugerat de articolul lui Austern „De ce nu ar trebui să utilizați set - și ce să utilizați în schimb”; David Smallberg mi-a îmbunătățit implementarea DataCompare. Descrierea containerelor din hash Dinkumware se bazează pe articolul lui Plauger „Hash Tables”. Mark Rogers nu este de acord cu concluziile sfatului 26, dar sfatul a fost inițial determinat de observația sa că unele funcții container acceptă doar argumente de tip iterator . Sfatul 29 a fost inspirat de discuțiile pe Usenet care au implicat Matt Ostern și James Kanze; Am fost influențat și de articolul „A Sophisticated Implementation of User-Defined Inserters and Extractors” de Klaus Kreft și Angelika Langer. Sfatul 30 se bazează pe secțiunea 5.4.2 din cartea lui Josattis The C++ Standard Library. În Sfatul 31, Marco Dalla Gasperina a oferit un exemplu de utilizare a elementului nth pentru a calcula mediana, iar utilizarea acestui algoritm pentru a găsi percentile este preluată direct din secțiunea 18.7.1 din Limbajul de programare C++ al lui Stroustrup. Sfatul 32 a fost inspirat de secțiunea 5.6.1 a cărții lui Josattis The C++ Standard Library. Sfatul 35 a fost inspirat de articolul lui Austern „Cum să faci o comparație de șiruri fără majuscule”, iar postările lui James Kanze și John Potter m-au ajutat să înțeleg mai bine ce se întâmplă. Implementarea copy_if din articolul 36 provine din Secțiunea 18.6.1 din Limbajul de programare C++ al lui Stroustrup. Sfatul 39 se bazează pe publicațiile lui Josattis, care a menționat pentru prima dată „predicatele cu stare” în cartea sa „The C++ Standard Library” și în articolul „Predicates vs. Obiecte funcție”. În cartea mea, folosesc exemplul lui și îi recomand soluția, deși termenul „funcție pură” este al meu. În sfatul 41, Matt Ostern mi-a confirmat suspiciunile cu privire la originea numelor mem_fun și mem_fun_ref. Sfatul 42 provine dintr-o prelegere susținută de Mark Rogers când am încălcat recomandarea acelui sfat. Mark Rogers a subliniat, de asemenea, la punctul 44 că căutările externe în containerele hărți și multihartă analizează ambele componente ale perechii, în timp ce căutările după funcțiile containerului iau în considerare doar prima componentă (cheia). Sfatul 45 folosește informații de la diverși colaboratori clem, inclusiv John Potter, Marcin Kasperski, Pete Becker, Dennis Yelle și David Abrahams. David Smallberg mi-a dat ideea de a folosi equal_range pentru a efectua căutări de echivalență și a număra rezultatele în containere ordonate secvenţial. Andrei Alexandrescu a ajutat la înțelegerea condițiilor pentru problema link-to-link menționată la sfatul 50; Exemplul dat în carte se bazează pe un exemplu similar al lui Mark Rogers preluat de pe site-ul web Boost.

Folosind containerul vectorial STL. Performanță vectorială
Utilizarea containerelor de bibliotecă standard C++ este întotdeauna condusă de simplitate, comoditate și performanță. Dar, ca orice „instrument”, acesta trebuie utilizat corect, deoarece un instrument bun și eficient în mâinile greșite poate fi inutil sau ineficient. În acest scurt articol, vă voi spune cum să îl utilizați cât mai eficient posibil și ce probleme și capcane pot apărea atunci când îl întâlniți pentru prima dată.

Să începem de la bun început
STL - Biblioteca de șabloane standard. Adică, tot ceea ce este exprimat prin șablon(e) în biblioteca standard este STL. Dar, în cea mai mare parte, STL este considerat a fi containere, algoritmi și iteratoare.
STL a fost inclus de mult în biblioteca standard C++, dar totuși nu toată lumea îl folosește pe deplin, în principal din cauza necunoașterii existenței anumitor clase de șabloane.

Și deci să revenim în mod specific la vector.
Un container vectorial este același cu o matrice care poate conține orice tip încorporat sau definit de utilizator. Dar, în comparație cu matricea încorporată, are o serie de avantaje.

El își cunoaște propria mărime (metoda dimensiune())
- Puteți modifica cu ușurință dimensiunea (adăugați, eliminați elemente) în timpul execuției
- La ștergere sau curățare, apelează destructori de clasă (ei bine, acest avantaj este discutabil =))

Să trecem peste principalele metode.

    push_back(element) - adaugă un element la sfârșitul vectorului
    pop_back(element) - elimina ultimul element al unui vector
    insert(***) - trei opțiuni (reîncărcare a metodei) pentru inserarea în orice zonă dintr-un vector, primul parametru este poziția de inserare dată iteratorilor, restul indică un container, sau o cantitate și un container, sau o pereche de iteratoare care indică din ce poziție dintr-un alt container preiau datele.
    erase(iterator sau iterator de la și către) - șterge un element sau o secvență de elemente dintr-un vector
    begin() - returnează un iterator care indică la începutul colecției.
    end() - returnează un iterator care indică sfârșitul colecției. În același timp, el nu indică chiar ultimul element, ci elementul imaginar din spatele ultimului.
    at(index) este o metodă de acces la elementele unei colecții, spre deosebire de operator, acesta verifică dacă depășește limitele colecției și, dacă da, generează o excepție.
    clear() - șterge toate elementele colecției și, dacă conține obiecte de clasă, apelează destructorii acestora. Dar dacă există pointeri către obiecte, atunci veți avea o scurgere de memorie (scurgeri de memorie =)), așa că nimeni nu va suna ștergere pentru dvs.
E timpul să-l vezi în acțiune!

Când iterați peste un vector prin iteratoare, trebuie să vă antrenați să scrieți un pre-increment( ++ea ), așa el mai efectiv.

Crearea unui vector static:

Cel mai frecvent caz de probleme cu crearea unui vector static este crearea acestuia într-o clasă.
Pentru a crea un vector static într-o clasă, trebuie să-l descrieți în .h și să-l definiți în .cpp. În acest caz, am pus totul într-un singur fișier, doar de dragul simplității. În acest fel puteți crea nu numai un vector static, ci și orice alt container static.

Acum despre eficiență

Un vector este eficient în cazurile în care aveți nevoie de un container în care veți acumula elemente. Adică adaugă. Un vector este eficient atunci când este adăugat la sfârșit folosind push_back() și când este îndepărtat de la sfârșit folosind pop_back().
Când inserați sau ștergeți într-un loc arbitrar (metodele insert() și erase()), eficiența vectorială este mai slabă comparativ cu lista( listă) sau o coadă cu două sensuri( deque). Acest lucru se datorează faptului că vectorul stochează date în memorie secvenţial, care oferă acces rapid aleator și abilitatea de a utiliza vectorul ca o matrice obișnuită. De exemplu, în aceleași funcții OpenGLÎn loc de un pointer către o matrice C, puteți specifica &vector_name.
Un vector are nu doar o dimensiune (size()), ci și o capacitate (capacity()), capacitatea este numărul maxim de elemente care, atunci când sunt adăugate, nu vor determina redistribuirea tamponului intern. Dacă această valoare este depășită, vectorul alocă un nou buffer intern (alocator) și copiază toate elementele din bufferul vechi în cel nou, în timp ce elimină din cel vechi. Adică, dacă stocăm în el obiecte de clasă mare, atunci această operațiune va fi destul de costisitoare. Acest lucru va fi „simțit” mai ales acut pe obiectele mari cu un destructor complex.
Capacitatea este specificată de implementarea bibliotecii standard în sine.

Dacă stocați pointeri către obiecte într-un vector, atunci acest lucru crește foarte mult eficiența stocării într-un vector, în special obiectele mari.
Stocarea obiectelor mici sau a tipurilor încorporate într-un vector este destul de eficientă.

Concluzie
Dacă nu intenționați să eliminați des elementele containerului și nu trebuie să introduceți elemente la început și la mijloc. Și veți stoca obiecte mici, tipuri încorporate sau indicatori către orice obiecte (mari sau nu atât de mari =)). Este mai bine să preferați un vector și să vă amintiți ce operațiuni se fac cu el, ce fac, în interiorul acestuia, printre altele. Și care dintre ele nu va fi prea scump.

Ți-a plăcut postarea? Păstrează-l pentru a reveni pentru a studia materialul!