LEZIONE 09 © C. Barberis, G. Malnati, 2004-14 Programmazione generica e Librerie C++ Anno Accademico 2013-14 Programmazione di Sistema I programmi tante volte non funzionano come ci aspetteremmo. Alcune volte possiamo predire le anomalie: se ci ricordiamo che l'utente si può dimenticare qualche campo, controlliamo con un if e via. Tuttavia esistono anomalie non predicibili, tipo la richiesta di troppa memoria, la somma di due numeri positivi che restituisce un numero negativo oppure problemi sull'I/O. Gestire le anomalie ¾ Non tutte le operazioni hanno sempre successo ¾ Alcuni sono predicibili © C. Barberis, G. Malnati, 2004-14 Possono verificarsi errori e fallimenti di varia natura Un utente non ha riempito un campo in un modulo I soldi disponibili in un conto corrente non sono sufficienti per coprire una data spesa ¾ Altri non lo sono Saturazione della memoria Overflow nel calcolo di un’operazione Disco pieno File assente Accesso negato ¾ Errori diversi possono essere trattati in modo differente Programmazione di Sistema 2 Gli errori attesi sono gestibili con tranquillità con if e costrutti analoghi. Attenzione però, perchè spesso ci si tende a dimenticare che determinate cose si possono controllare (come la scanf: mi hai chiesto di leggere un intero, ci sono riuscito?). scanf se ne accorgerebbe anche, ma siccome non sa a cosa servono quei dati, la cosa migliore che può fare è avvisare. Se tu però non te ne accorgi sei fritto. Errori attesi ¾ Occorre verificare se siano o meno presenti ¾ Problemi © C. Barberis, G. Malnati, 2004-14 Gestendoli nel punto preciso in cui sono stati identificati A volte il test viene omesso ( int scanf(char*, …) ) Altre volte, l’errore viene rilevato in una funzione che non sa come trattarlo: occorre propagarlo all’indietro ¾ In tante situazioni, poi, non è nemmeno possibile fare la propagazione (non posso far tornare qualcosa di informativo al costruttore). Problemi legati alla propagazione Una funzione potrebbe ritornare già un valore Un costruttore non può ritornare nulla Cosa capita se il chiamante di una funzione non propaga un errore? Programmazione di Sistema 3 Con gli errori non attesi non posso fare una sbrodolata di if, perchè si possono verificare ovunque. Se poi negli if andiamo anche a gestire il tutto non ne usciamo mai più. Meglio demandare a qualcuno gerarchicamente superiore la gestione di situazioni indesiderate, in quanto laddove si verifica l'emergenza magari non ci sono le competenze. Errori non attesi ¾ Molto più difficili da gestire Possono verificarsi pressoché ovunque Il tentativo di gestirli rende praticamente illeggibile il codice © C. Barberis, G. Malnati, 2004-14 ¾ È molto improbabile che sia possibile gestirli nel posto in cui si sono verificati Se non si riesce ad allocare un oggetto interno ad un’implementazione mentre si invoca un metodo, cosa fare? ¾ Occorre un modo alternativo per poter strutturare il codice Che permetta di fare fronte alle evenienze là dove si ha la possibilità di prendere una decisione opportuna Programmazione di Sistema 4 Per questo sono state introdotte le eccezioni: si parte dal punto dove c'è stata la turba, tornando indietro di N (imprecisati) livelli, fin quando incontriamo qualcuno in grado di gestire l'emergenza. Tutto il codice intermedio viene saltato. In questo modo si evitano gli if e si scrive il codice come se fosse giusto. Se dovesse nascere qualche turba, si chiede di segnalarla. Sarà poi l'infrastruttura di gestione delle eccezioni a tornare indietro quanto basta. Vantaggi: - siccome lo fa il compilatore non ci sono dimenticanze - riesce a farlo con un grande livello di ottimizzazione: manipolando a basso livello riesce a tagliare pezzi di stack Eccezioni (1) © C. Barberis, G. Malnati, 2004-14 ¾ Meccanismo che permette di trasferire il flusso di esecuzione di un programma ad un punto precedente dove si ha la possibilità di gestirlo Saltando tutto il codice intermedio Ed evitando il rischio di dimenticarsi di propagare una indicazione di errore ¾ Si notifica al sistema la presenza di un’eccezione creando un dato qualsiasi E passandolo come valore alla parola chiave “throw” Quando il sistema esegue “throw”, abbandona il normale flusso di elaborazione e inizia la ricerca di una contromisura Programmazione di Sistema 5 Eccezioni (2) ¾ A throw si può passare quello che vogliamo, a condizione che sia copiabile. La libreria standard offre una classe std::exception che si può estendere per rappresentare quello che vogliamo. Aumenta la leggibilità, quindi perchè non usarla? Il tipo del dato passato a throw è arbitrario © C. Barberis, G. Malnati, 2004-14 Serve a descrivere quanto si è verificato Il suo tipo viene utilizzato per scegliere quale contromisura adottare ¾ La libreria standard offre la classe std::exception Può essere usata come base di una gerarchia di ereditarietà, per creare classi più specifiche ¾ Occorre documentare bene l’uso delle eccezioni Evidenziandone la semantica Programmazione di Sistema 6 Le contromisure si adottano scrivendo dei blocchi di tipo try/catch. Occhio che non c'è il finally. try: qui c'è una zona con delle contromisure. Se nell'esecuzione di qualunque istruzione (che può essere anche una chiamata a funzione) nel blocco try si verifica un'eccezione, l'esecuzione si stoppa, lo stack viene contratto e si prosegue nel blocco catch coerentemente col tipo di dato throwato. Contrarre lo stack mi permette non solo di ricavare qual era l'indirizzo dell'istruzione a cui sarei dovuto tornare all'interno del try, ma distrugge anche tutte le variabili locali distribuite invocando il distruttore. © C. Barberis, G. Malnati, 2004-14 Eccezioni (3) try { //le istruzioni eseguite //qui possono lanciare //un’eccezione } catch (out_of_range& oor){ //contromisura //per errore specifico } catch (exception& e) { //contromisura generica } ¾ Se un’eccezione si verifica in un blocco try… …o in un metodo chiamato dall’interno di un blocco try… ¾ …si contrae lo stack fino al blocco try incluso Tutte le variabili locali vengono distrutte ed eliminate Programmazione di Sistema 7 Eccezioni (4) ¾ Le clausole “catch” effettuano un match basato sul tipo dell’eccezione che si è verificata Se la classe dell’eccezione coincide o deriva da (is_a) quella indicata nel blocco catch, il codice corrispondente viene eseguito e l’eccezione rientra © C. Barberis, G. Malnati, 2004-14 Gestire l'eccezione vuol dire identificare l'anomalia e scrivere un blocco di codice che prenda contromisure. Come descriviamo un' anomalia? Con un dato qualsiasi (sarebbe furbo metterci i dati utili), al contrario di Java dove si usano oggetti Throwable. Una volta creato l'oggetto (anche un int, anche se non è furbissimo) lo passo alla parola chiave throw che permette di dire: "interrompi l'esecuzione e torna indietro fin quando non trovi qualcuno che sappia gestire eccezioni del tipo passato". ¾ I blocchi catch sono esaminati nell’ordine in cui sono scritti ¾ Se non c’è alcuna corrispondenza, l’eccezione rimane attiva e il programma ritorna alla funzione chiamante… Non è detto che l'esecuzione corrisponda a uno dei blocchi catch indicati, contraendo lo stack fino ad arrivare al main. A quel punto il programma termina. Occorre ordinarli dall’eccezione più specifica alla più generica …ed eventualmente al chiamante del chiamante… Fino ad incontrare un blocco catch adatto… …o fino alla contrazione completa dello stack, con la terminazione del flusso di esecuzione Programmazione di Sistema Posso mettere più di un blocco catch dopo un try. I blocchi catch vengono esaminati nell'ordine in cui sono riportati, facendo un match di tipo is_a (l'eccezione verificata è esattamente la stessa classe o una sottoclasse?). In questo modo posso scrivere anche cose ben specifiche. Importante scrivere prima lo specifico e poi il generico. 8 Dentro a exception abbiamo una stringa, utile al programmatore per aiutare il debugging. Mai fare controlli sulla stringa. La classe std::exception ¾ - logic error: il programma si è trovato in una situazione inconsistente e predicibile - runtime error: condizioni inattese (system error: ho chiesto all'OS di fare questo e l'OS ha risposto picche. All'interno di system error poi ho un codice che mi identifica l'errore particolare) Incapsula una stringa, che viene inizializzata nel costruttore © C. Barberis, G. Malnati, 2004-14 Si accede al suo contenuto con la funzione membro what() Serve per documentare l’errore, NON per creare del codice che reagisca all’eccezione ¾ Alcune classi derivate std::logic_error – incoerenza del codice Nei nostri casi conviene estendere logic o runtime. ¾ Sottoclassi: domain_error, invalid_argument, length_error, out_of_range,… std::runtime_error – condizione inattesa ¾ Sottoclassi: overflow_error, range_error, underflow_error ¾ std::system_error – errore nell’accesso al sistema operativo: incapsula un oggetto di tipo std::error_code ¾ Si possono derivare ulteriori sottoclassi Programmazione di Sistema 9 © C. Barberis, G. Malnati, 2004-14 Gerarchia di std::exception Programmazione di Sistema 10 Se vogliamo usare exception dobbiamo includere il file exception: gli include file della libreria di sistema del C++ sono senza il .h. Dichiarare le eccezioni ¾ Le classi std::exception e suoi derivati sono dichiarate in diversi file #include <exception> ¾ std::exception e std::bad_exception #include <stdexcept> ¾ Per la maggior parte delle classi relative agli errori logici e in fase di esecuzione #include <system_error> © C. Barberis, G. Malnati, 2004-14 ¾ Per gli errori di sistema (C++11) #include <new> ¾ Per le eccezioni relative alla mancanza di memoria #include <ios> ¾ Per gli errori di I/O #include <future> ¾ Per gli errori relative all’esecuzione asincrona (C++11) #include <typeinfo> ¾ Per le eccezioni legate ai cast dinamici o alla RTTI Programmazione di Sistema 11 Boh, la sintassi try/catch è ragionevole. Ma nel catch cosa vado a fare. Al solito dipende da cosa succede. - pianto tutto: attenzione, non con exit(). Termino in modo ordinato e via - ritento l'esecuzione: supponiamo di aver provato ad accedere a una risorsa di sistema occupata... proviamoci di nuovo! - scrivo in un log e poi rilancio l'eccezione, lasciando che siano altri a gestirla Cosa fare nel blocco catch ¾ Ha lo scopo di rimediare all’eccezione che si è verificata © C. Barberis, G. Malnati, 2004-14 In base al tipo di evento, occorre adottare una contromisura adeguata ¾ Possibili strategie Terminare, in modo ordinato, il programma Ritentare l’esecuzione, evitando di entrare in un loop infinito Registrare un messaggio nel log e rilanciare l’eccezione Programmazione di Sistema 12 Quello che si è detto. Nel catch salvo, pulisco, rilascio e finisco con exit, che permette di indicare un codice di ritorno per il nostro processo, coerentemente con quanto specifica il sistema operativo. Script più esterni possono vedere quel valore di ritorno per decidere cosa fare. Terminare il programma © C. Barberis, G. Malnati, 2004-14 ¾ Si procede a salvare lo stato del programma e rilasciare eventuali risorse globali Azione estrema, di solito lasciata al livello più esterno (main) ¾ La sintassi “…” indica un’eccezione qualsiasi Permette l’ingresso nel blocco catch qualunque sia il tipo di dato lanciato (che però non è accessibile) try { //azioni a rischio } catch( ... ) { // eventuali azioni di // salvataggio catch(...): qualunque eccezione si sia verificata. O ammazzo tutto o faccio un throw; (sintassi lecita solo nei blocchi catch con ...) //codice di errore exit(-1); } Programmazione di Sistema 13 Questo è un esempio di come posso tentare esecuzioni ripetute. Ritentare l’esecuzione © C. Barberis, G. Malnati, 2004-14 ¾ Strategia adatta quando il fallimento è dovuto a cause temporanee Congestione di rete Mancanza di risorse (disco, memoria) se l’applicazione può rilasciarne ¾ L’istruzione “throw” al termine del blocco catch rilancia l’eccezione catturata int retry=2; while (retry) { try { //azioni varie break; //termina } catch(exception& e) { ReleaseExtraRes(); retry--; if (!retry) throw; } } Programmazione di Sistema 14 E questo invece è un esempio di log di eventuali rogne. Naturalmente dopo aggiungiamo throw, perchè scrivere senza prendere provvedimenti non è il massimo della furbizia. Registrare un messaggio © C. Barberis, G. Malnati, 2004-14 ¾ Si usa un meccanismo opportunamente predisposto per la registrazione degli errori Come il flusso “std::cerr” Si può utilizzare il metodo what() di exception per accedere ad una descrizione dell’errore ¾ try { //… } catch (exception& e) { logger::log(e.what()); throw; } L’istruzione throw finale garantisce che l’eccezione venga propagata Programmazione di Sistema 15 Attenzione a non fare blocchi catch in cui non si prendano contromisure. Gran vaccata. Cosa non fare ¾ Scrivere un blocco catch che non esegue nessuna strategia di riallineamento © C. Barberis, G. Malnati, 2004-14 E lascia che il programma, la cui esecuzione è parzialmente fallita, prosegua ¾ Stampare un messaggio di errore e poi continuare, in genere, non è una soluzione try { //… } catch (std::exception& std::exception& exception& e) { std::cerr<<“Errore:”<<e.what()<<“\n”; <<“Errore:”<< } Programmazione di Sistema 16 Contrazione dello stack ¾ Il fatto che lo stack venga contratto in fase di lancio di un’eccezione è alla base di un pattern di programmazione tipico del C++ ¾ Aiuta a garantire che le eventuali risorse acquisite in fase di costruzione siano liberate © C. Barberis, G. Malnati, 2004-14 RAII – Resource Acquisition Is Initialization In questo modo l'oggetto entrerà nell'ecosistema generale in ogni caso. E' l'alternativa al blocco finally di Java. Può essere usato per eseguire un insieme di azioni anche in presenza di eccezioni Facendo attenzione a non sollevarne delle altre ¾ E’ l’unica alternativa al blocco “finally” di Java, che in C++ non esiste Programmazione di Sistema 17 RAII class ActionTimer { { long start; ActionTimer at(“b1”); for(int i=0; i<8; i++){ ActionTimer at2(“b2”); //do something… throw std::exception; } std::string msg; public: ActionTimer(std::string tag): msg(tag){ © C. Barberis, G. Malnati, 2004-14 Il fatto che lo stack venga contratto è una cosa potentissima che permette di scrivere codice pulito, ma va capita. Il pattern basato sulla contrazione dello stack si chiama RAII (Resource Acquisition Is Inizialization): se un nostro oggetto ha bisogno di risorse esterne (file, allocazione memoria, apertura di socket...) fa queste operazioni nel costruttore e il distruttore poi provvederà a rilasciare tutto. start=GetCurrentTime(); } ~ActionTimer() { } long t=GetCurrentTime()-start; Esempio banale: ogni tanto abbiamo bisogno di misurare dei tempi. Un modo possibile è quello di prepararsi un oggetto il cui unico scopo è fare due operazioni, una in costruzione (guardo che ore sono e me lo segno) e una in distruzione (guardo quanto tempo è passato e mando un certo messaggio). Il codice a destra mi fa vedere 8 messaggi b2 e 1 b1 (questo perchè non l'abbiamo creato con new, ma come variabile locale). Se in uno dei cicli del for viene lanciata un'eccezione, lo stack si contrae: il ciclo corrente viene comunque misurato. Vedrò comunque il blocco b1. std::cout<<msg<<“:”<<t<<“\n”; } }; Programmazione di Sistema 18 Gestire le eccezioni è una cosa buona perchè se non le lanciamo non abbiamo grosse penalità a livello di efficienza. Al contrario, se l'eccezione viene lanciata il costo è 10 volte maggiore, motivo per cui non devo usare eccezioni in allegria. I costi delle eccezioni ¾ Se nessuna eccezione viene lanciata, non ci sono sostanziali penalità © C. Barberis, G. Malnati, 2004-14 La definizione di un blocco try si limita ad inserire alcune informazioni nello stack ¾ Se viene lanciata un’eccezione il costo è abbastanza elevato Almeno un ordine di grandezza maggiore dell’invocazione di una funzione a cui si aggiunge il costo dell’esecuzione dei vari distruttori ¾ Non è un meccanismo conveniente per gestire logiche locali Un costrutto “if” è molto meno oneroso Programmazione di Sistema 19 In C/C++ esiste un sistema di tipi che permette di non fare porcate. Tuttavia esiste il cast. In Java non possiamo far diventare List una String, non c'è verso. Buono, ma da una grossa penalità: la scelta di rendere non cogente il sistema dei tipi in C++ permette di operare a basso livello dove alcune violazioni controllate sono lecite (a casa non mi devono sfondare la porta, ma se ho un incendio ben vengano i vigili del fuoco). Programmazione generica ¾ Un sistema di tipi stringente facilita la creazione di codice robusto © C. Barberis, G. Malnati, 2004-14 Identificando, in fase di compilazione, quei frammenti di codice che portano a violazioni del sistema dei tipi ¾ In certe situazioni, per non violare il sistema dei tipi occorre replicare una grande quantità di codice Ciò genera problemi in fase di manutenzione, in quanto occorre garantire che eventuali modifiche ad una versione del codice vengano propagate a tutte le altre Programmazione di Sistema 20 Programmazione generica (2) © C. Barberis, G. Malnati, 2004-14 ¾ Il linguaggio C++ offre un supporto alla generalizzazione dei comportamenti di un blocco di codice attraverso il concetto di “template” Frammenti (funzioni, classi) parametrici che vengono espansi in fase di compilazione, in base al loro uso effettivo Non richiedono controlli in fase di esecuzione ¾ Permettono di implementare una varietà di funzionalità diverse adattandole ai singoli tipi di dato Algoritmi per il valore minimo, massimo, medio, … Collezioni di dati coerenti per il tipo incapsulato e relativi algoritmi Spesso basate sulla ridefinizione degli operatori ¾ 21 Il caso più semplice è quello di una funzione. T è il nome generico (una classe). Diciamo che se T è un tipo, ci inventiamo una funzione max che prende due T per reference e restituisce un oggetto T. L'assunzione è che T sia un tipo qualsiasi, confrontabile tramite un minore. Se abbiamo bisogno di usarla in qualche contesto, dobbiamo dire quale specializzazione vogliamo creare. Quando il compilatore vede ad esempio i = max(10,20) fa tutti i controlli del caso e istanzia il template sostituendo a T la parola chiave int. In classi che implemento io dev'esserci l'operator "<" implementato affinchè l'esempio funzioni. Funzioni generiche ¾ Si può definire una funzione in modo che operi su un tipo di dato non ancora precisato © C. Barberis, G. Malnati, 2004-14 template <class T> T max(T& t1, T& t2) { return (t1< t2 ? t2 : t1); } ¾ Opera sul tipo generico “T” Di esso, il compilatore deduce che si tratta di un tipo (<class T>) che supporta l’operatore “<” tra argomenti omogenei e la costruzione per copia (per via del valore ritornato) ¾ Quando si usa la funzione, il compilatore – per ogni uso – determina quale tipo effettivo assegnare a “T”, in base ai parametri passati int i=max(10,20); // T ß int std::string s, s1 = …, s2= …; s = max( s1, s2); // T ß std::string max<double>(2, 3.1415928); //forza la scelta di “T” a double Programmazione di Sistema < In questo caso il compilatore ha dei dubbi perchè passiamo un int e un double. Meglio specificare cosa devo usare. 22 Classi generiche ¾ Questa genericità la esprimo mediante template, ovvero un frammento di codice che al suo interno contiene placeholder senza tipo predefinito. Questo tipo verrà acquisito in fase di compilazione: sarà cura del compilatore fare le copie, senza macelli vari. Permettono di implementare tanti tipi di funzionalità adattandole ai singoli tipi (ricerca del massimo, ...). Tutta la libreria standard del C++ è basata sull'utilizzo dei template. La parte principale della libreria standard è basata su template Programmazione di Sistema Come evitiamo rogne? Il C++ offre un meccanismo di supporto alla generalizzazione dei comportamenti, con un costrutto un po' stravagante. L'idea di base è: quando scrivo un algoritmo posso farlo (entro certi limiti) per qualunque tipo che abbia determinate caratteristiche. Anche con le classi si può fare sostanzialmente la stessa cosa. Qui ho una classe che lavora con un tipo generico T (sommabile e copiabile). Quando utilizzo una classe di questo tipo indico il tipo specifico. Lo stesso principio può essere adottato nella definizione delle classi © C. Barberis, G. Malnati, 2004-14 Quando queste vengono usate, si correda il tipo generico con l’indicazione della specializzazione richiesta template <class T> class Accum { T total; public: Accum(T start): total(start) {} T operator+=( const T& t) { return total= total+t; } T value() { return total;} }; Accum<std::string> sa(“”); Accum<int> ia(0); azione di Sistem Programmazione Sistema 23 La classe stack permette di gestire delle pile di oggetti T con una dimensione fissata a priori. © C. Barberis, G. Malnati, 2004-14 Template C++ template <class T, int size> class Stack { public: Stack() { } ~Stack() { } ¾ void push(T val); T pop(); ¾ Evita di ridefinire classi per tipi di dati o dimensioni differenti private: T data[size]; int index; }; Stack<int,100> s; Stack<double,30> s1; s2; Stack<CProva,20> Programmazione dii Si Sistema Il parametro può essere un identificatore per un tipo di dato o un valore costante Ogni volta che si istanzia un template (indicando il valore dei parametri), il compilatore genera una nuova classe/ funzione opportunamente modificata Soluzione migliore rispetto all’uso di gerarchie di ereditarietà 24 Ci sono delle volte in cui un certo tipo non è direttamente usabile dentro un template. La funzione max presupponeva che il dato fosse confrontabile con l'operatore minore. Specializzare un template (1) © C. Barberis, G. Malnati, 2004-14 ¾ Alcuni tipi potrebbero non essere utilizzabili all’interno di un dato template Se non posso modificare la classe del tipo aggiungendo un operatore, devo usare una specializzazione (ovvero una definizione specifica per la classe non altrimenti usabile). Ad esempio, potrebbero non offrire l’implementazione di un operatore usato nella definizione del template stesso ¾ Se non è possibile modificare la classe del tipo, fornendo le implementazioni richieste (o la semantica richiesta), si può specializzare il template Ovvero fornirne una definizione specifica per la classe non altrimenti usabile Programmazione di Sistema 25 Person dentro di sè non ha l'operatore +=. Se io non posso modificare Person posso fare una specializzazione del template fornendo una mia implementazione del +=. © C. Barberis, G. Malnati, 2004-14 Specializzare un template (2) class Person { std::string name; public: Person(std::string n): name(n) {} std::string name() { return name; } }; template<> class Accum<Person> { int total; public: Accum(int start = 0): total(start) {} int operator+=(const Person &) {return ++total;} int value() { return total;} }; Programmazione di Sistema < Dalla lista del template<> tolgo gli elementi che sto specificando e li aggiungo dopo. 26 Vantaggi: - espando le capacità espressive - nessuna penalità temporale in tempi di codice eseguito - risparmio sul tempo di sviluppo - risparmio in fase di debug - non mette a rischio il sistema dei tipi - lascia spazio a future evoluzioni del codice Template: punti di forza e di attenzione ¾ Aumentano notevolmente le possibilità espressive di un programmatore Senza pregiudicare i tempi di esecuzione Risparmiano tempo di sviluppo Non mettono a rischio il sistema dei tipi Introducono flessibilità in vista di futuri miglioramenti © C. Barberis, G. Malnati, 2004-14 ¾ Chi usa un template, deve verificarne le assunzioni sui tipi effettivamente usati Quando si usa un template bisogna verificare le assunzioni fatte (cosa vuol dire +=?). Se ho qualche rogna posso giocarmela con la specializzazione. Non solo a livello sintattico (lo fa già il compilatore), ma soprattutto a livello semantico Eventuali casi particolari possono essere gestiti mediante la specializzazione ¾ Eventuali violazioni sul tipo utilizzato, per lo più conducono ad errori di compilazione difficilmente interpretabili ¾ Spesso, non occorre scrivere nuovi template L'espansione del template conduce ad espressioni anche molto differenti dal codice originale Il più delle volte si usano template già fatti. Bene capirli per diventare skillati. Occorre però sapere usare MOLTO BENE quelli esistenti Programmazione di Sistema 27 Smart Pointer ¾ La ridefinizione degli operatori permette di dare una semantica alle operazioni ‘*’ e ‘->’ anche a dati che non sono (solo) puntatori © C. Barberis, G. Malnati, 2004-14 In questo modo è possibile costruire oggetti che “appaiono come” puntatori ma che hanno caratteristiche ulteriori ¾ ¾ ¾ ¾ ¾ Garanzia di inizializzazione e rilascio Conteggio dei riferimenti Accesso controllato … Possono anche essere ridefinite operazioni legate all’aritmetica dei puntatori (++, --, …), al confronto (==, !=, !, …) ed all’assegnazione/copia (=) per aumentare il parallelo con i puntatori semplici Meglio dunque usare questi oggetti che hanno la stessa sintassi dei puntatori e che sottendono tutta una serie di complessità per gestire copiabilità e altro 28 LEZIONE 12 Come possiamo usare il meccanismo dei template per risolvere un problema spinoso: eliminare i puntatori nativi (con tutto l'impiccio che ne deriva). Gli smart pointer si basano su due meccanismi: - uso dei template - il C++ ci consente di ridefinire operatori presenti nel linguaggio affinchè assumano, con tipi diversi, significati diversi. Il puntatore nativo ha la capacità di fare riferimento a un dato che sta altrove. Soffre però di una serie di limitazioni fondamentali: essendo copiabile e assegnabile posso fare ptr1 = ptr2. Il problema è che se una classe contiene un puntatore nativo, copiabilità e assegnabilità impattano su costruttore di copia e operatore di assegnazione. Se non faccio niente e copio un oggetto, entrambi puntano a una stessa zona di memoria. Non bene. Nell'esempio dobbiamo manipolare un puntatore a intero. Definiamo la classe int_ptr. Contiene, ovviamente, un puntatore nativo. Privatamente dichiaro costruttore di copia e operatore di assegnazione, in modo che non sia nè copiabile e nè assegnabile. Esempio © C. Barberis, G. Malnati, 2004-14 class int_ptr { int* ptr; int_ptr(const int_ptr&); int_ptr& operator=( const int_ptr&); public: explicit int_ptr(int* p) : ptr(p) { } ~int_ptr() {delete ptr;} int& operator*() {return *ptr;} ¾ Questa classe incapsula un puntatore ad un intero Che si suppone essere allocato sullo heap ¾ ¾ Quando un oggetto di questo tipo viene distrutto la memoria viene rilasciata automaticamente Si potrebbero inserire controlli nel costruttore per verificare che il puntatore non sia nullo }; Programmazione di Sistema 29 Generic smart_ptr © C. Barberis, G. Malnati, 2004-14 template <class T> class smart_ptr { T* ptr; smart_ptr(const smart_ptr<T>&); smart_ptr<T>& operator=(const smart_ptr<T>&); public: explicit smart_ptr(T* p = 0) : ptr(p) {} ~smart_ptr() { delete ptr; } T& operator*() { return *ptr; } T* operator->() { return ptr; } Nella parte pubblica metto il costruttore (in modo che il puntatore sia creabile): metto explicit affinchè il compilatore pensi che sia un modo per convertire gli int* in int_ptr. Naturalmente poi c'è anche il distruttore (che fa l'assunzione molto forte "il puntatore è allocato nell'heap). Il pezzo interessante è la ridefinizione di operator*(): "tutte le volte che uso *v sto facendo *ptr". Se uso questa classe fatta così posso avere una serie di comportamenti interessanti: lanciare eccezioni nel caso in cui *p sia NULL e distruggere la memoria a cui punta l'oggetto nel momento in cui l'oggetto viene distrutto. Fatto per gli interi serve a poco. Meglio combinarlo con i template, in modo da avere genericità. Anche qui nei private ho puntatore nativo, costruttore di copia e operatore di assegnazione (quindi non copiabile). Ho un costruttore con un parametro di default (se non mi passi niente assumo NULL), un distruttore e la ridefinizione di operatore * e operatore ->. Non perdiamo l'accessibilità al puntatore ma manteniamo l'incapsulamento: grosso vantaggio. }; Programmazione di Sistema © C. Barberis, G. Malnati, 2004-14 Codici a confronto void esempio1() { MyClass* p = new MyClass(); p->Esegui(); delete p; } void esempio2() { smart_ptr<MyClass> p( new MyClass() ); p->Esegui(); } smart_ptr si occupa di chiamare l’operatore delete sul puntatore dell’oggetto incapsulato quando si esce dal blocco, evitando perdite di memoria, anche in caso di eccezioni! A sinistra, tipico ciclo di vita di un puntatore nativo. A destra, uso di smart pointer. Non dichiaro più un puntatore nativo, ma uno smart_ptr. Il puntatore lo inizializzo con new MyClass(). Quando mi serve il puntatore farò esattamente come prima, perchè sintatticamente è utilizzabile come un puntatore. Non ho più la responsabilità di chiamare delete perchè l'ho trasferita nell'oggetto smart_ptr. Vantaggi: - tolgo ambiguità alle operazioni di copia (non si fanno): se mi capita una situazione in cui ho bisogno di gestire una copia, la gestisco in modo consapevole - tolgo la responsabilità del rilascio, trasferita nell'oggetto smart pointer: la cosa è ottima perchè gestisce anche problemi in caso di eccezioni Programmazione di Sistema Sicurezza per le eccezioni ¾ La funzione Esegui() potrebbe generare un’eccezione © C. Barberis, G. Malnati, 2004-14 le righe di codice successive non verrebbero eseguite ¾ L’uso di costrutti try/catch può fare esplodere le dimensioni del programma L’uso dello smart pointer garantisce che la risorsa venga rilasciata automaticamente Programmazione di Sistema void esempio1() { MyClass* p(new MyClass); p->Esegui(); delete p; } void esempio3() { MyClass* p; try { p = new MyClass; p->Esegui(); delete p; } catch (...) { delete p; throw; } } Nell'esempio1(), Esegui potrebbe chiamare un'eccezione senza eseguire una delete. p sparisce dalla vista e nessuno è in grado di rilasciarlo. Senza usare gli smart pointer devo usare ovviamente del try/catch: di per sè il meccanismo è corretto, ma è un macello (se ogni volta che ho un puntatore devo fare tutto sto giro non se ne esce più, vedi esempio3()). esempio2(), con smart pointer: viene generata un'eccezione in Esegui. Lo stack si contrae perchè non c'è un try/catch e vengono chiamati i distruttori di tutte le variabili di esempio2(), quindi anche quello di p #muchbetter. Fin qui abbiamo fatto costruttore di copia e operatore di assegnazione privati, per tenere le cose semplici. Copia e smart pointer ¾ Il secondo problema legato ai puntatori (oltre al fatto che la semantica base di copia sia inadeguata) è il possesso. Quando io ho un puntatore nativo, è lui il proprietario dell'area di memoria? E' quindi responsabile del rilascio? Se devo fare copie chi è che fa il rilascio? A chi appartiene la memoria puntata da uno smart pointer? © C. Barberis, G. Malnati, 2004-14 Il problema si pone nel caso si voglia assegnare uno smart pointer ad un altro ¾ È possibile implementare strategie diverse in funzione delle necessità Passaggio di proprietà Creazione di una copia Condivisione con conteggio dei riferimenti Condivisione in lettura e duplicazione in scrittura … Programmazione di Sistema 33 Ownership-Handling ¾ © C. Barberis, G. Malnati, 2004-14 ¾ ¾ Se l’oggetto, tramite lo smart pointer, viene usato da un unico utilizzatore, esso può essere eliminato al termine delle operazioni dall’unico possessore della risorsa Se l’oggetto, tramite lo smart pointer, viene usato da più utilizzatori è necessario definire chi possa eliminare la risorsa Questo può essere l’ultimo a conoscere l’oggetto Un garbage collector che opera per conto del sistema Tutto questo va sotto il nome di ownership-handling: possiamo creare classi di smart pointer differenti che quando è ora di copiarsi fanno cose diverse. Se ho uno smart pointer che punta a una zona che dev'essere conosciuta solo da quest'ultimo, ha senso che costruttore di copia e operatore di assegnazione siano privati e che il blocco sia rilasciato quando lo smart pointer è distrutto. Se, viceversa, noi vogliamo mettere in atto una strategia di condivisione operatore di assegnazione e costruttore di copia non devono essere privati e devono implementare una strategia in cui conoscano quanti riferimenti ci sono a quel puntatore. Oppure si può creare un garbage collector che analizza tutta la memoria alla ricerca di oggetti allocati ma non più utilizzati (molto appealing per il programmatore, meno per la macchina perchè stoppa tutto). Programmazione di Sistema Smart pointer nella libreria standard ¾ A partire dalla versione C++11, sono disponibili diversi template per la gestione automatica della memoria ¾ std::shared_ptr<BaseType> ¾ std::weak_ptr<BaseType> © C. Barberis, G. Malnati, 2004-14 #include <memory> Implementa un meccanismo di conteggio dei riferimenti Permette di osservare il contenuto di uno shared_ptr senza partecipare al conteggio dei riferimenti ¾ std::unique_ptr<BaseType> Implementa il concetto di proprietà, impedendo la copia ma permettendo il trasferimento Programmazione di Sistema 35 std::shared_ptr (1) ¾ © C. Barberis, G. Malnati, 2004-14 La standard library ci mette a disposizione una serie di smart pointer: quelli interessanti sono in C++ 11 (Visual Studio 2012). - shared_ptr: implementa al suo interno conteggio dei riferimenti. E' copiabile ed assegnabile. Quando copio, dunque, condivido con consapevolezza (non c'è rischio che il blocco che sto puntando mi sparisca sotto il naso) - weak_ptr: si può usare solo in congiunzione con uno shared e permette di creare "cicli". - unique_ptr: questo è un puntatore di cui voglio essere l'unico possessore. Impedisce la copia ma permette il trasferimento con la std::move Mantiene la proprietà condivisa ad un blocco di memoria referenziato da un puntatore nativo Molti oggetti di questo tipo possono referenziare lo stesso blocco Quando tutti sono stati distrutti o resettati, il blocco viene rilasciato ¾ Per default, il blocco referenziato viene rilasciato tramite l’operatore delete In fase di costruzione di shared_ptr, è possibile specificare un meccanismo di rilascio alternativo ¾ Un oggetto di questo tipo può anche non contenere alcun puntatore valido Se è stato inizializzato o resettato al valore NULL Programmazione di Sistema 36 shared_ptr L'idea è che il blocco di memoria è condiviso tra tanti shared_ptr e quando tutti sono distrutti il blocco è rilasciato. Quando costruiamo shared_ptr possiamo dire che noi non vogliamo delete come operazione di rilascio, ma un'altra cosa. Può contenere null, magari perchè abbiamo usato il costruttore di default senza parametri o perchè abbiamo usato reset. Posso poi usare il costruttore che parte da un puntatore nativo. Al termine, il conteggio dei riferimenti vale 1. Il distruttore decrementa il conteggio e libera la memoria solo se ho 0 riferimenti. get(): ritorna T*, con la libertà di usarlo come voglio nel bene o nel male. std::shared_ptr (2) ¾ shared_ptr<T>( T* native_ptr) Costruisce uno shared_ptr che incapsula il puntatore native_ptr © C. Barberis, G. Malnati, 2004-14 ¾ ~shared_ptr<T>() Distrugge uno shared_ptr e libera la memoria se il contatore dei riferimenti è giunto a 0 ¾ operator=(const shared_ptr<T>& other) Assegna il puntatore condiviso, incrementando il contatore ¾ get(), operator* (), operator-> () Permettono di accedere al puntatore incapsulato Programmazione di Sistema 37 make_shared: creo un'istanza della mia classe allocata sull'heap. Ottimo quando voglio essere sicuro che il puntatore nativo venga realmente dall'heap. std::shared_ptr (3) ¾ reset() © C. Barberis, G. Malnati, 2004-14 Decrementa il contatore ed elimina il riferimento contenuto nello smart pointer Se il contatore scende a 0, libera la memoria ¾ reset(BaseType* ptr) Come sopra, ma sostituisce il puntatore contenuto con quello passato come parametro ¾ make_shared<BaseType>(params…) Funzione di supporto che provvedere a creare sullo heap un’istanza della classe BaseType usando i parametri passati come parametri del costruttore e ad incapsularla in uno shared_ptr Programmazione di Sistema 38 Dettagli di funzionamento. Ho il puntatore nativo e il puntatore a un blocco di controllo, in cui c'è: - quanti conoscono l'oggetto - quanti weak_ptr fanno parte dell'oggetto - un riferimento all'oggetto stesso std::shared_ptr: implementazione tipica objectPtr object ob © C. Barberis, G. Malnati, 2004-14 counterPtr co counter =3 we weakCnt = 0 objectPtr ob objectPtr counterPtr ¾ objectPtr counterPtr ¾ Programmazione di Sistema Og shared_ptr punta ad un Ogni bloc blocco di controllo oltre che all’ all’oggetto stesso La dimensione dello smart pointer raddoppia Inoltre occorre memorizzare il blocco di controllo Una volta c'era solo auto_ptr, che con l'uscita del C++ 11 è stato deprecato. Questo perchè implementa una logica implicita di ownership transfer (creo un auto_ptr a partire da un puntatore nativo: se lo copio in un altro, il puntatore nativo va nell'oggetto di destinazione e in quello di sorgente ci va null... non proprio il massimo). Quindi nel C++ 11 si sono aggiunti i comportamenti di cui sopra. std::auto_ptr<BaseType> © C. Barberis, G. Malnati, 2004-14 ¾ Il puntatore incapsulato negli oggetti di tipo auto_ptr appartiene a tali oggetti La memoria cui fa riferimento deve appartenere allo heap ed essere stato allocato tramite new All’atto della distruzione di auto_ptr, viene invocato il metodo delete ¾ Cosa capita se si duplica un oggetto di tipo auto_ptr, attraverso un’assegnazione o un costruttore di copia? La proprietà del puntatore viene trasferita al destinatario L’oggetto sorgente viene modificato, ponendo a NULL il puntatore incapsulato Altrimenti si rischierebbe di invocare delete due volte! Programmazione di Sistema Cosa succede quando costruisco una copia? Nel nuovo blocco mi ricopio sia objectPtr che counterPtr, poi faccio counter++. Quando distruggo faccio counter--: se non è 0 amen, se è 0 faccio la delete seguendo l'objectPtr contenuto. Uno sharedPtr costa dunque il doppio di memoria rispetto a un puntatore nativo, ma da il doppio di vantaggi. 40 shared_ptr è interessante ma soffre del problema delle dipendenze cicliche: A conosce B, B conosce C, C conosce A. Quando l'ultimo riferimento ad A, B o C viene rimosso, il contatore non raggiunge mai 0 perchè A, ad esempio contiene uno smart_ptr a B e così via... Dipendenze cicliche ¾ Il conteggio dei riferimenti garantisce il rilascio della memoria in modo deterministico Per questo motivo è stata creata la classe weak_ptr: quando devi creare dei cicli (e lo sai) decidi che la tua relazione, invece di essere completamente pari è asimmetrica (il padre conosce il figlio tramite uno shared_ptr, il figlio conosce il padre con un weak_ptr) © C. Barberis, G. Malnati, 2004-14 Non appena un oggetto non ha più riferimenti viene liberato ¾ In alcuni casi, tuttavia, non funziona Se si forma un ciclo di dipendenze (A®B, B®A) il contatore non può mai annullarsi, anche se gli oggetti A e B non sono più conosciuti da nessuno ¾ Occorre evitare la creazione di cicli ricorrendo a std::weak_ptr<BaseType> Programmazione di Sistema 41 Il weak_ptr mantiene il riferimento al padre ma non conteggia il riferimento. Nel momento in cui più nessuno conosce il padre il riferimento del weak_ptr viene settato a NULL. Per accedere al weak_ptr devo promuoverlo: quando ho bisogno di risalire al padre devo promuovere il weak_ptr a uno shared_ptr, per poi retrocederlo al suo grado base. Per promuoverlo uso lock(). Lo shared_ptr quando si distrugge sparisce. Il lock() può fallire se il padre non esiste più: posso testare la validità con expired() onde evitare eccezioni. std::weak_ptr (1) © C. Barberis, G. Malnati, 2004-14 ¾ Mantiene al proprio interno un riferimento “debole” ad un blocco custodito in uno shared_ptr Modella il possesso temporaneo ad un blocco di memoria ¾ Si acquisisce temporaneamente l’accesso al dato tramite il metodo lock() che restituisce uno shared_ptr Si verifica la validità del riferimento con il metodo expired() Programmazione di Sistema 42 Cominciamo dal main. Nel C++ è stata introdotta la parola chiave auto ("compilatore, sostituisci ad auto il tipo giusto sulla base di quello che c'è dall'altra parte dell'uguale", si usa per dichiarare variabili inizializzate). sp è uno std::shared_ptr, ed è dunque un puntatore a intero (e il puntatore punta a un blocco di memoria in cui c'è scritto 42). A questo punto gw = sp, con gw weak_ptr. Il weak_ptr aggiunge in una sua variabile privata il riferimento allo shared_ptr: il numero dei riferimenti al blocco non è cresciuto. Chiamo f() una prima volta: provo a fare gw.lock() (= costruire uno shared pointer). Dereferenzio spt e stampo 42. Dopo la } distruggo sp: il contatore dei riferimenti scende a 0, il blocco di memoria che contiene 42 viene liberato. gw è globale. Viene richiamata f(), gw.lock() ritorna null che viene valutato false. gw è scaduto. © C. Barberis, G. Malnati, 2004-14 std::weak_ptr (2) std::weak_ptr<int> gw; void f(){ if (auto spt = gw.lock()) std::cout<<"gw:"<< *spt << "\n"; else std::cout<< "gw e' scaduto\n"; } int main() { { auto sp = std::make_shared<int>(42); gw = sp; f(); } // sp viene distrutto, gw scade f(); } Prog Programmazione di Sistema 43 std::unique_ptr © C. Barberis, G. Malnati, 2004-14 ¾ Incapsula il puntatore ad un singolo oggetto o ad un array di oggetti Non può essere copiato né assegnato Il puntatore può essere trasferito esplicitamente ad un altro unique_ptr attraverso la funzione std::move() ¾ ¾ Quando viene distrutto, il blocco viene rilasciato Usi tipici Garantire la distruzione di un oggetto anche in caso di eccezioni Passare la proprietà di un oggetto con ciclo di vita dinamico tra funzioni Gestire in modo sicuro oggetti polimorfici Programmazione di Sistema 44 Puntatori nativi incapsulati in smart pointer non condivisibili. Possiamo trasferirne il contenuto chiamando move(). Quando viene distrutto, il blocco viene rilasciato. Un uso tipico è quando abbiamo un puntatore nativo e vogliamo che in presenza di eccezioni venga liberato. Ottimo anche quando voglio gestire oggetti polimorfici, che usano puntatori nativi. I/O in C++ Certamente possiamo fare printf. Non facciamolo. Questo perchè printf e soci conoscono solo i tipi elementari e non ci danno supporto per tipi più complicati. C++ invece offre un meccanismo sofisticato che ci permette di gestire l'I/O di oggetti qualunque. Input/Output in C++ ¾ È possibile utilizzare le funzioni della libreria stdio.h ¾ Il C++ fornisce un’alternativa per la gestione dei flussi di input e di output printf, scanf, ecc… Come in Java, l'assunzione base è l'uso di uno stream (input o output), indipendentemente da/a chi è destinata. Qui abbiamo istream (generico flusso da cui si legge) e ostream (generico flusso da cui si scrive) e iostream (leggo e scrivo). Da queste strutture basi derivano alcune classi concrete specializzate per operare su file (ifstream, ofstream, fstream) o su un buffer di memoria (istringstream, ostringstream, stringstream). © C. Barberis, G. Malnati, 2004-14 ios istream istringstream ifstream ostream iostream fstream ostringstream ofstream stringstream Programmazione di Sistema 45 ios: classe che contiene una serie di costanti più alcune variabili istanza comuni a tutti gli stream che ci dicono lo stato dello stream (lo stream è in uno stato buono o in uno stato d'errore? l'errore è recuperabile), alcune informazioni sulla manipolazione del formato (gli interi magari li scrivo esadecimali, metto spaziature automatiche...). Si noti che ios non può essere istanziata direttamente, ma serve sapere cosa c'è perchè tutti vi si appoggiano. Nessuna operazione lancia un'eccezione: è un bene perchè non devo fare try/ catch, è un male perchè devo fare i test sullo stato dello stream. Gerarchia di classi di I/O (1) ¾ ios è la classe base (virtuale) e contiene attributi e metodi comuni a tutti gli stream © C. Barberis, G. Malnati, 2004-14 stato dello stream: (errori ricuperabili, errori irricuperabili, fine del flusso, …) manipolazione del formato non può essere istanziata direttamente ¾ istream e ostream eseguono rispettivamente le operazioni di input ed output su flussi generici metodi di lettura e scrittura dello stream operatori >> e << accesso indiretto allo stream tramite buffer (allocato internamente) ¾ istream/ostream: classi generiche per operazioni di lettura e scrittura. Ci opero con >> (getFrom) e << (putTo). Buono perchè concatenabile ed evito di creare stringhe in memoria (come in Java col +), in certe situazioni è una fregatura. iostream esegue operazioni di lettura e scrittura Programmazione di Sistema 46 Di base noi lavoriamo o direttamente sulla console (std::cin e std::cout, leggo da tastiera o scrivo su video) oppure ifstream e ofstream quando voglio operare con file. La prima permette di indicare il nome del file dal quale vogliamo leggere, la seconda permette di indicare il nome del file e la modalità (overwrite/append). Gerarchia di classi di I/O (2) ¾ Operazioni su file le classi base sono ifstream e ofstream © C. Barberis, G. Malnati, 2004-14 ¾ derivano (indirettamente) da ios ¾ contengono metodi per la creazione dello stream (open, close) le classi derivate definiscono stream per l’input e l’output ¾ ifstream ! accesso in sola lettura ¾ ofstream ! accesso in sola scrittura ¾ fstream ! accesso allo stream sia in lettura che in scrittura ¾ Esistono classi analoghe per le operazioni di lettura e scrittura su stringhe istringstream, ostringstream, stringstream Programmazione di Sistema 47 Esempi di cout, con l'operatore << (putTo). L'operatore << ritorna sempre l'oggetto che è alla propria sinistra, quindi posso fare una catena lunga a piacere. E' promuovibile a boolean tramite una conversione interna e possiamo sapere se lo stream è good o bad. Output su video (1) © C. Barberis, G. Malnati, 2004-14 ¾ Per le operazioni di output si utilizza la variabile std::cout l’operatore << le funzioni fornite dalla classe ostream using namespace std; char ch=‘a’; cout<<ch; int i=10; cout<<i; float f=1.1; cout<<f; char* s=“ab”; cout<<s; if (!cout) { … } Programmazione di Sistema // scrive un carattere // scrive un intero // scrive un float // scrive una stringa // Errore in scrittura 48 Volendo ostream offre anche metodi a basso livello. Questi metodi non interpretano il significato dei caratteri, mentre << è polimorfico (a seconda di quello che mettiamo, sceglie il significato giusto). Output su video (2) ¾ Operazioni fornite dalla classe ostream scrittura di un carattere ch qualsiasi © C. Barberis, G. Malnati, 2004-14 ¾ char ch=‘a’; cout. put(ch); scrittura di una blocco di byte lungo al più n ¾ cout.write(s,n); Programmazione di Sistema 49 cout.width(4); /* Stampa almeno 4 caratteri */ precision va bene per i numeri, nel caso di double o float: quanti numeri dopo la virgola andiamo a considerare? I/O con formato © C. Barberis, G. Malnati, 2004-14 ¾ Funzione width: ampiezza del campo opera sull’istruzione di scrittura successiva e definisce il numero minimo di caratteri da impiegare cout.width(4); cout<<‘a’; // stampa " " " a ¾ Funzione precision: precisione è applicabile ai numeri reali e definisce il numero massimo di caratteri da impiegare cout.precision(4); cout<<1.4142 // stampa 1.414 Programmazione di Sistema 50 Manipolatori © C. Barberis, G. Malnati, 2004-14 ¾ Particolari oggetti, detti manipolatori, possono essere inviati in scrittura o in lettura ad un flusso permettono di cambiare il formato di rappresentazione, inserire o estrarre particolari dati nel flusso Definiti nel file <iomanip> using namespace std; int i = 10; cout<<i<<“ (0x”<<hex<<setfill(‘0’)<<setw(4)<< i <<“)”<<endl; 10 (0x000a)¿ Programmazione di Sistema 51 © C. Barberis, G. Malnati, 2004-14 Manipolatori di formato ¾ ¾ ¾ ¾ ¾ ¾ ¾ ¾ ¾ ¾ ¾ ¾ ¾ skipws: ignora gli spazi left: allinea a sinistra right: allinea a destra internal: segno a sinistra del campo, valore a destra dec: intero base decimale oct: intero base ottale hex: intero base esadecimale showpoint:reale, usa punto decimale flush: svuota il buffer in uscita endl: invia a cout il carattere ‘\n’ ends: invia a cout il carattere ‘\0’ ws: produce l’eliminazione degli spazi in input … Programmazione di Sistema 52 Abbiamo anche wcin, wcout, wcerror e wclog che operano con caratteri a 16 bit (wide char). Concatenando tante volte >> preleviamo più volte dati dal flusso. Input da tastiera (1) © C. Barberis, G. Malnati, 2004-14 ¾ Le operazioni primarie di I/O avvengono attraverso variabili standard std::cin - flusso standard di ingresso std::cout - flusso standard di uscita std::cerr - flusso standard di errore (privo di buffer) std::clog - flusso standard di errore (con buffer) ¾ Per le operazioni di input si utilizza la variabile std::cin l’operatore >> le funzioni fornite dalla classe istream ¾ Utilizzando il costrutto using namespace std; si può omettere il prefisso std:: Programmazione di Sistema 53 Apprezza il polimorfismo, capra! Input da tastiera (2) ¾ Nella lettura con l’operatore >> © C. Barberis, G. Malnati, 2004-14 cin>>ch; ignora gli eventuali spazi iniziali cin>>s; ignora gli spazi iniziali e si arresta al primo carattere di spaziatura, la stringa viene completata con \0 nel caso di errore o di EOF il valore restituito da (!cin) è 0 char ch; cin>>ch; int i; cin>>i; float f; cin>>f; char s[100]; cin>>s; // legge un carattere //legge un intero //legge un float //legge una stringa (fino ad //uno spazio, tab, return,…) if(!cin) cout << “Errore in lettura”<<endl; Programmazione di Sistema 54 Funzioni a basso livello. Input da tastiera (3) ¾ Funzioni fornite dalla classe istream lettura di un carattere ch qualsiasi © C. Barberis, G. Malnati, 2004-14 ¾ cin.get(ch); lettura di una stringa di caratteri lunga al più n-1 ¾ cin.getline(s,n); lettura di un blocco di dati in formato binario ¾ cin.read(s,1000); Programmazione di Sistema 55 Di base lo stream è promuovibile a booleano in modo da sapere se lo stream è buono o in stato di errore. Possiamo poi vedere se c'è end of file e tante altre cose. Con clear ripristiniamo le cose. Gestione dello stato dello stream ¾ A differenza di quanto avviene in Java, in C++ le operazioni di IO non sollevano eccezioni Il programmatore deve testare esplicitamente il risultato di ogni operazione effettuata © C. Barberis, G. Malnati, 2004-14 ¾ Gli indicatori seguenti registrano la condizione che si è verificata a seguito dell’ultima operazione di I/O eof() ! true se è stato incontrato EOF fail() ! true se è avvenuto un errore di formato, ma che non ha causato la perdita di dati bad() ! true se c’è stato un errore che ha causato la perdita di dati good() ! true se non c’è stato alcun errore ¾ Il metodo clear ripristina lo stato del flusso Programmazione di Sistema 56 © C. Barberis, G. Malnati, 2004-14 Esempio (1) #include <iostream> #include <iomanip> using namespace std; int main ( ) { int a, b, c = 8, d = 4; float k ; char name[30] ; cout << "Enter your name" << endl ; cin.getline (name, 30) ; cout << "Enter two integers and a float " << endl ; cin >> a >> b >> k ; if (! cin.good()) return -1; //error Programmazione di Sistema 57 © C. Barberis, G. Malnati, 2004-14 Esempio (2) cout << "\nThank you, " << name << ", you entered“ << endl << a << ", " << b << ", and " ; cout.width (4) ; cout.precision (2) ; cout << k << endl ; // Modo alternativo per controllare l’output cout <<"\nThank you, " << name << ", you entered" << endl << a << ", " << b << ", and " << setw (4) << setprecision (2) << k << endl ; return 0; } Programmazione di Sistema 58 Esempio - Output Enter your name R. J. Freuler Enter two integers and a float © C. Barberis, G. Malnati, 2004-14 12 24 67.857 Thank you, R. J. Freuler, you entered 12, 24, and ø 68 Thank you, R. J. Freuler, you entered 12, 24, and ø 67.85 Programmazione di Sistema 59 © C. Barberis, G. Malnati, 2004-14 Esempio – I/O da file #include <fstream> using namespace std; int main ( ) { int a, b, c ; ifstream fin ; //creazione dell’input stream fin.open ( "my_input.dat") ; //apertura del file fin >> a >> b ; //lettura di due interi if (!fin) return -1; // equivalente a if (!fin.good())… c=a+b; ofstream fout ; //creazione di un file di output fout.open ( "my_output.dat"); // apertura del file fout << c << endl ; //scrittura del risultato fin.close ( ) ; //chiusura input file fout.close ( ) ; //chiusura output file return 0; } Programmazione di Sistema 60 Standard Template Library ¾ Il comitato di standardizzazione del C++ ha definito un insieme di funzionalità, espresse attraverso template, comuni alle diverse implementazioni ¾ La libreria STL è caratterizzata da © C. Barberis, G. Malnati, 2004-14 Standard Template Library classi contenitore, i cui oggetti sono collezioni omogenee di altri oggetti ¾ vector, deque, list, set, multiset, map, multimap, string algoritmi generici ¾ ricerca, fusione, ordinamento,… ¾ è necessario che il tipo di dato su cui viene applicato abbia definite specifiche operazioni (ad esempio ==, <, >) ¾ Per accedere agli elementi di una classe contenitore si utilizzano gli iteratori oggetti di tipo cursore che si comportano come puntatori Programmazione di Sistema 61 Contenitori (1) ¾ Ci sono due tipi di contenitori: sequenziali e associativi. Nei primi si mantiene l'ordinamento intrinseco, legato all'ordine con cui sono stati aggiunti. array tradizionale: buono perchè ha accessi a tempo costante, ma ho larghezza fissa. Se la nostra collezione ha dati variabili nel tempo, l'array non è una soluzione furba: esiste il vector che è un array che si allarga al bisogno. vector ottimi quando ho garanzia di aggiungere e togliere solo in coda. deque: parte dal mezzo e poi allunga da una parte e dall'altra. Tempo di accesso costante a un elemento qualunque, tempo di inserimento in testa e in coda pressochè costante, tempo di inserimento in mezzo variabile. list: linked list doppiamente collegata, ogni elemento punta al precedente e al successivo. Ho un puntatore alla testa e uno alla coda. I contenitori di tipo sequenziale organizzano linearmente una collezione di oggetti array “tradizionale” ¾ fornisce un accesso casuale a tempo costante ad una sequenza di lunghezza fissa © C. Barberis, G. Malnati, 2004-14 vector ¾ fornisce un accesso casuale a tempo costante, con tempo di inserimento e cancellazione costante in coda deque ¾ fornisce un accesso casuale a tempo costante, con tempo di inserimento e cancellazione costante in testa e in coda list ¾ fornisce un accesso casuale con tempo lineare e tempo di inserimento e cancellazione costante in qualsiasi posizione Non c'è un contenitore più figo degli altri, bisogna capire quale è adatto al nostro scopo. Programmazione di Sistema 62 Oltre ai contenitori sequenziali ho i contenitori associativi, nei quali si accede tramite una chiave che può essere un oggetto quasi qualunque (dev'essere const, non mutabile nel tempo). - set: collezione di elementi in cui la chiave è unica - multiset: ammetto duplicazione delle chiavi - map: separo chiave e oggetto di riferimento - multimap: ammessa la presenza di chiavi duplicate Contenitori (2) ¾ I contenitori di tipo associativo forniscono l’accesso agli oggetti della collezione tramite chiavi set ¾ la chiave deve essere unica © C. Barberis, G. Malnati, 2004-14 multiset ¾ la chiave può essere duplicata map ¾ basata su coppie chiave, valore ¾ la chiave deve essere unica multimap ¾ la chiave può essere duplicata Programmazione di Sistema 63 Liste Sequenza di dati omogenea con tutte le operazioni di gestione (inserisci, cerca, cancella, ordina...). All'interno c'è una lista doppiamente linkata. In generale posso lavorare su uno qualunque degli elementi a cui faccio riferimento tramite un iteratore. Liste (1) ¾ Sono costituite da sequenze di dati omogenei Incapsulano tutte le operazioni di gestione forniscono una sintassi ed una semantica simile a quella dei tipi elementari ¾ © C. Barberis, G. Malnati, 2004-14 LEZIONE 15 Il concetto di programmazione generica risulta particolarmente utile nel suppporto della cosiddetta Standard Template Library. Pur essendo abbastanza esigua, è la parte più corposa della libreria C++. Cosa c'è dentro? 1) un insieme di classi contenitore: permettono la realizzazione di algoritmi che manipolano collezioni di dati (liste, set, mappe, ...) In Java siccome tutto deriva da Object è in teoria possibile fare liste miste, in C++ dobbiamo aggiungere un parametro che specializza il template (e quindi metto oggetti di un solo tipo). Gestione furba delle stringhe per tenere conto dei vari locale durante la lettura/scrittura dei file, dato che string è modellato come istanza di un template. 2) insieme di funzioni template, che prendono il nome di algoritmi: sort, merge, ordering... 3) iteratori Un oggetto di tipo list incapsula una lista doppiamente collegata di oggetti dal punto di vista dell'utente è una sequenza di oggetti che su cui è possibile ¾ inserire nuovi oggetti ¾ cancellare gli oggetti ¾ esaminarne il contento si accede agli elementi di una lista tramite un iteratore Programmazione di Sistema 64 Quando creiamo la lista mettiamo tra <> il tipo che ci mettiamo dentro. Attenzione: il tipo di dato dev'essere copiabile e assegnabile, ovvero deve avere costruttore di copia e operatore di assegnazione e questi devono essere funzionalmente equivalenti. Pessima idea fare oggetti list di puntatori: questo perchè i puntatori sono copiabili e assegnabili, ma lo perdiamo di vista (se proprio ne abbiamo bisogno, utilizziamo shared_ptr del nostro dato. Non unique_ptr). Liste (2) ¾ Le liste contengono dati omogenei e generici ogni lista può contenere un solo tipo di oggetti ¾ viene specificato alla creazione della lista ¾ deve supportare copia e assegnazione © C. Barberis, G. Malnati, 2004-14 il compilatore impedisce che vengano inseriti dati di un tipo inappropriato ¾ Per utilizzare le liste è necessario includere il file <list> definisce i template che sono usati per dichiarare e creare le liste (che appartengono allo spazio dei nomi “std”) #include <list> using namespace std; ¾ Se usiamo le liste dobbiamo includere il file <list>. In certe situazioni possiamo aggiungere "using namespace std", che ogni tanto è fonte di guai perchè diciamo al compilatore: "prova a mettere std:: davanti a tutto quello che non capisci". E tra l'altro MAI MAI MAI scriverlo nei .h. //evita di dover scrivere std::list<…> Si dichiara una lista istanziandone il template list<int> w; list<string> y; Programmazione di Sistema 65 1) Creo, nel contesto in cui mi trovo, una lista vuota 2) Creo una lista con capacità iniziale 3 interi, ottenuti dal costruttore di default di int 3) Simile a 2, creo a partire da un valore specificato. 4) Le liste, internamente, fanno la new di pezzi di memoria (e la delete quando non servono più). Di default loro usano la new standard, ma ci possono essere situazioni in cui si usano dati frequentemente creati o distrutti: si possono istruire gli oggetti list dicendo di far riferimento a un oggetto allocatore. 5) la lista è copiabile ed assegnabile, quindi noi possiamo creare una lista a partire da un'altra: occhio che l'operazione è gravosa Creazione di liste (3) ¾ Lista vuota ¾ Lista con n elementi con il valore di default ¾ Lista con n elementi del valore specificato ¾ Utilizzando l’allocatore di un’altra lista ¾ Copiando un’altra lista list <int> c0; © C. Barberis, G. Malnati, 2004-14 list <int> c1( 3 ); // 0 0 0 list <int> c2( 5, 2 ); // 2 2 2 2 2 list <int> c3( 3, 1, c2.get_allocator( ) ); list <int> c4(c2); // 1 1 1 // 2 2 2 2 2 Programmazione di Sistema 66 Concettualmente un iteratore è un indice all'interno di un contenitore e rappresenta un elemento contenuto da qualche parte. Esegue al suo interno l'overload di operator * e quindi si comporta come un puntatore. Posso passare all'elemento successivo (e precedente), confrontare due iteratori con eguaglianza o diversità. Non posso confrontare con < o >, perchè non so dove sono piazzati in memoria. Iteratori ¾ Oggetti che indicano una posizione all’interno di un contenitore Si accede all’oggetto dereferenziando l’iteratore (come fosse un puntatore) © C. Barberis, G. Malnati, 2004-14 ¾ Se si incrementa un iteratore (operator++()), si avanza all’elemento successivo Viceversa, si torna all’elemento precedente se lo si decrementa (operator--()) ¾ Due iteratori possono essere confrontati per eguaglianza (operator==()) o diversità (operator!=()) Programmazione di Sistema 67 L'iteratore più semplice che possiamo immaginare è il puntatore nativo. Nell'esempio vogliamo cercare il valore 18 all'interno di un array. La prima casella dell'array è puntata da array, l'ultima da array + 10. Puntatori come iteratori • © C. Barberis, G. Malnati, 2004-14 • L’iteratore più semplice è un puntatore ad un blocco di oggetti Si delimita il blocco con due puntatori – L’inizio (incluso) – La fine (esclusa) • • In un contenitore vuoto, inizio e fine coincidono Dereferenziare il puntatore finale è un errore class C; C array[10]; C *iter = array; C *end = array+10; for( ; iter != end; iter++) { C elem = *iter; //… } – Si punterebbe oltre il limite dell’array Programmazione di Sistema 68 Se non ho un array elementare ma ho un oggetto list non cambia praticamente niente: devo usare iteratori di inner class iterator come definiti nella classe list. begin è la posizione iniziale, end è la prima posizione oltre la fine. Tutto identico. Generalizzare gli iteratori ¾ © C. Barberis, G. Malnati, 2004-14 ¾ Il codice precedente può essere generalizzato Invece che operare su puntatori, può operare su oggetti di altro tipo purché possano essere confrontati incrementati dereferenziati class C; list<C> l(10); Gli iteratori devono essere confrontabili per uguaglianza e differenza, devono essere incrementabili e deferenziabili (detto in altri termini devono supportare ==, !=, ++, * e facoltativamente ->). list<C>::iterator iter = l.begin(); list<C>::iterator end = l.end(); for( ; iter != end; iter++) { C elem = *iter; //… } Programmazione di Sistema 69 Il codice base dell'algoritmo che fa la ricerca (il for) non è cambiato, è cambiato il tipo di dato dell'iteratore. Ogni contenitore ha il suo proprio iteratore: l'iteratore di una lista non è confrontabile con l'operatore di un vector. L'unica cosa lecitamente confrontabile sono i dati a cui gli iteratori puntano. Generalizzare gli iteratori ¾ Il codice dell’algoritmo non è cambiato © C. Barberis, G. Malnati, 2004-14 È cambiato il tipo di dato usato per scorrere il contenitore ¾ Il compilatore si occupa di selezionare le corrette operazioni (definite nella classe dell’iteratore) per eseguire confronti, incrementi e accesso al contenuto Ogni contenitore offre un proprio tipo di iteratore in grado di implementare la necessaria semantica Programmazione di Sistema 70 L'oggetto list mette a disposizione - push_back: aggiunge una copia dell'elemento passato come parametro al fondo della lista - push_front: come push_back ma in testa - back: restituisce elemento presente in coda (ne fa una copia) - front: come back ma in testa Liste – Operazioni (1) ¾ ¾ © C. Barberis, G. Malnati, 2004-14 ¾ ¾ push_back aggiunge un elemento alla fine della lista push_front aggiunge un elemento all’inizio della lista back restituisce il riferimento all’ultimo elemento della lista front restituisce il riferimento al primo elemento della lista list <int> c1; c1.push_back( 10 ); c1.push_back(11); int& i = c1.back( ); int& j = c1.front( ); Programmazione di Sistema 71 - erase: cancello un elemento o una serie di elementi a partire da un iteratore (nell'esempio 10 va via, resta solo il 20) - insert: inserisco a partire da un iteratore (nell'esempio inserisco tra 10 e 20) Liste – Operazioni (2) ¾ erase rimuove un elemento o una serie di elementi a partire dalla posizione specificata © C. Barberis, G. Malnati, 2004-14 c1.push_back( 10 ); c1.push_back( 20 ); c1.erase( c1.begin( ) ); ¾ insert inserisce uno o più elementi nella lista nella posizione specificata list <int>::iterator Iter; c1.push_back( 10 ); c1.push_back( 20 ); Iter = c1.begin( ); Iter++; c1.insert( Iter, 100 ); Programmazione di Sistema 72 - clear: elimina tutti gli elementi nella lista - empty: lista vuota? - size: quanti elementi ci sono? - max_size: numero massimo degli elementi che la lista può contenere. Di base può contenere MAX_INT, ma potrebbe dare un numero diverso a seconda dell'allocatore che viene usato - pop_back: cancella l'ultimo elemento della lista MA NON LO RESTITUISCE - pop_front: come pop_back, ma col primo elemento Liste – Operazioni (3) ¾ clear elimina tutti gli elementi della lista ¾ empty verifica se la lista è vuota ¾ size restituisce il numero di elementi nella lista c1.clear( ); bool b = c1.empty( ); © C. Barberis, G. Malnati, 2004-14 list <int>::size_type l = c1. size( ); ¾ max_size restituisce il numero massimo di elementi che la lista può contenere ¾ pop_back cancella l’ultimo elemento della lista ¾ pop_front cancella il primo elemento della lista list <int>::size_type m = c1.max_size( ); c1.pop_back( ); c1.pop_front( ) Programmazione di Sistema 73 - remove_if: meccanismo che ci deve diventare familiare. Liste – Operazioni (3) © C. Barberis, G. Malnati, 2004-14 ¾ Accetta come parametro due cose: 1) il nome della funzione (che il compilatore poi traduce come indirizzo): remove_if scorre la lista, invoca la funzione e passa come parametro alla funzione l'elemento i-esimo. Se la funzione restituisce true, l'elemento viene cancellato 2) meno banale se passiamo come parametro un oggetto, in cui all'interno è ridefinito operator (). L'oggetto è dunque usabile come una funzione. Il compilatore si rende conto che non riceve una funzione, ma qualcosa che è utilizzabile come una funzione (perchè dentro di sè ridefinisce il suo comportamento). Qui le cose che fa sono stupidissime, ma in alcuni casi sono molto furbe remove_if cancella tutti gli elementi per cui è verificata la condizione specificata class is_odd : public std::unary_function<int, bool> { public: bool operator( ) ( int val ) { return ( val % 2 ) == 1; } }; int main( ) { list <int> c1; c1.push_back( 3 ); c1.push_back( 4 ); c1.push_back( 5 ); c1.push_back( 6 ); c1.push_back( 7 ); c1.push_back( 8 ); c1.remove_if( is_odd( ) ); // c1 = 4 6 8 } Programmazione di Sistema (vedi esempio) 74 - merge: fonde due liste in un'unica lista. Per default è riordinata in ordine crescente. Si sorta la lista 1, si sorta la lista 2 e poi viene applicato l'algoritmo di fusione. Liste – Operazioni (4) © C. Barberis, G. Malnati, 2004-14 ¾ merge elimina gli elementi della lista passata come argomento e li inserisce nella lista destinazione, riordinandola in ordine crescente (o nell’ordine specificato) list <int> c1, c2, c3; list <int>::iterator c1_Iter, c2_Iter, c3_Iter; c1.push_back( 3 ); c1.push_back( 6 ); c2.push_back( 2 ); c2.push_back( 4 ); c3.push_back( 5 ); c3.push_back( 1 ); c2.merge( c1 ); // 2 3 4 6 c2.merge( c3, greater<int>( ) ); // 6 5 4 3 2 1 Programmazione di Sistema 75 - remove: cancello l'elemento passato come parametro. - resize: permette di cambiare la dimensione della lista, o buttando via gli ultimi dopo la posizione indicata o inserendo tanti elementi di default fin quando la lunghezza non raggiunge quella indicata. Liste – Operazioni (5) © C. Barberis, G. Malnati, 2004-14 ¾ remove cancella l’elemento passato come parametro c1.push_back( 5 ); c1.remove(5); ¾ resize specifica la nuova dimensione della lista c1.push_back( 10 ); c1.push_back( 20 ); c1.push_back( 30 ); c1.resize( 6,40 ); // c1 = 10,20,30,40,40,40 Programmazione di Sistema 76 - reverse: "ribalta" la lista - sort: ordina la lista a condizione che l'elemento dentro la lista supporti operator <. E' possibile anche passare un algoritmo come parametro per indicare una direzione diversa. Liste – Operazioni (5) ¾ reverse inverte l’ordine degli elementi ¾ sort ordina gli elementi in ordine ascendente o secondo l’ordinamento specificato © C. Barberis, G. Malnati, 2004-14 c1.reverse( ); c1.push_back( 20 ); c1.push_back( 10 ); c1.push_back( 30 ); c1.sort(); c1.sort( greater<int>( ) ); // c1 = 10 20 30 // c1 = 30 20 10 Programmazione di Sistema 77 - splice: permette di togliere una serie di elementi a partire dalla lista di partenza, inserendoli in una lista di destinazione senza fare copie (sconnette puntatori dalla lista 1 e li riconnette nella lista 2) Liste – Operazioni (6) splice cancella gli elementi dalla lista passata come argomento e li inserisce nella lista destinazione © C. Barberis, G. Malnati, 2004-14 ¾ list <int> c1, c2; c1.push_back( 10 ); c1.push_back( 11 ); c2.push_back( 12 ); c2.push_back( 20 ); c2.push_back( 21 ); c2.splice(c2.begin( ), c1); // c2 = 10 11 12 20 21 Programmazione di Sistema 78 - unique: rimuove i duplicati adiacenti Liste – Operazioni (7) ¾ unique rimuove gli elementi adiacenti duplicati © C. Barberis, G. Malnati, 2004-14 c1.push_back( -10 ); c1.push_back( 10 ); c1.push_back( 10 ); c1.push_back( 20 ); c1.push_back( -10 ); c1.push_back( 20 ); c1.sort(); //c1 = -10-10 10 10 20 20 c1.unique( ); // c1 = -10 10 20 Programmazione di Sistema 79 - swap: esiste in due versioni. La prima scambia i contenuti di c1 e c2. La seconda invece prende come parametro due liste, ma funziona ugualmente. Liste – Operazioni (7) © C. Barberis, G. Malnati, 2004-14 ¾ swap scambia gli elementi di due liste c1.push_back( 1 ); c1.push_back( 2 ); c1.push_back( 3 ); c2.push_back( 10 ); c2.push_back( 20 ); c3.push_back( 100 ); c1.swap( c2 ); // c1 = 10 20 swap( c1,c3 ); // c1 = 100 Programmazione di Sistema 80 Mappe: servono per modellare relazioni chiavi-valori. Tipicamente una mappa base associa ad una chiave un solo valore. Le chiavi devono essere immutabili. Internamente il contenuto è rappresentato con un albero binario bilanciato. Mappe (1) © C. Barberis, G. Malnati, 2004-14 ¾ Modellano il concetto di relazione tra due insiemi di oggetti, detti rispettivamente chiavi e valori Una mappa associa, ad ogni chiave, uno ed un solo valore Le chiavi devono essere oggetti immutabili ¾ Per utilizzare le mappe è necessario includere il file <map> #include <map> Programmazione di Sistema 81 La mappa permette di relazionare elementi di un primo insieme (chiavi) con elementi del secondo (valori). Le chiavi devono essere distinte, i valori non necessariamente. Mappe (2) Valori © C. Barberis, G. Malnati, 2004-14 k1 k2 k3 Chiavi v1 k6 v2 k4 v3 v4 k5 v5 Mappa Programmazione di Sistema 82 La mappa è sorted associative, perchè mantiene gli elementi nell'ordine definito da operator <. E' pair associative, perchè permette di associare a una generica chiave il suo valore (data una mappa posso usarla come se fosse un array, utilizzando direttamente un elemento di tipo chiave). Posso avere iteratori che operano su oggetti di tipo pair. Mappe (3) ¾ Una mappa è un contenitore: di tipo sorted associative ¾ mantiene gli oggetti ordinati in base alle loro chiavi di tipo contenitore di tipo pair associative © C. Barberis, G. Malnati, 2004-14 ¾ pair <const Key, Data> di tipo unique associative ¾ due elementi non possono avere la stessa chiave ¾ Per accedere agli elementi di una mappa, si utilizzano gli iteratori l’inserimento di un elemento non invalida gli iteratori esistenti che puntano a elementi esistenti la cancellazione di un elemento non invalida gli iteratori, tranne quelli che puntano all’elemento cancellato Programmazione di Sistema 83 Occhio che chiavi e valori devono essere assignable (copiabili e assegnabili): questo perchè la mappa internamente si fa copia degli oggetti. pair <t1,t2> ¾ ¾ Coppia di elementi eterogenei t1 e t2 devono essere modelli di tipo Assignable © C. Barberis, G. Malnati, 2004-14 un tipo di dato è Assignable se ¾ è possibile copiare gli oggetti ¾ è possibile assegnare il valore degli oggetti a variabili ¾ Per accedere al primo elemento della coppia ¾ Per accedere al secondo elemento della coppia pair.first; pair.second; Programmazione di Sistema 84 Possiamo inserire nella mappa degli elementi... Operatori (1) ¾ insert – inserisce uno o più elementi in una mappa © C. Barberis, G. Malnati, 2004-14 map <int, int> m1; typedef pair <int, int> Int_Pair; m1.insert ( Int_Pair ( 0, 0 ) ); m1.insert ( Int_Pair ( 1, 1 ) ); m1.insert ( Int_Pair ( 2, 4 ) ); Programmazione di Sistema 85 ... possiamo iterare da begin... Operatori (2) ¾ begin – restituisce l'iteratore alla posizione del primo elemento della mappa © C. Barberis, G. Malnati, 2004-14 map <int, int> m1; map <int, int> :: iterator m1_Iter; typedef pair <int, int> Int_Pair; m1.insert ( Int_Pair ( 0, 0 ) ); m1.insert ( Int_Pair ( 1, 1 ) ); m1.insert ( Int_Pair ( 2, 4 ) ); m1_Iter = m1.begin ( ); Programmazione di Sistema 86 ... fino a end. Operatori (3) ¾ end – restituisce un iteratore alla posizione successiva all'ultimo elemento della mappa © C. Barberis, G. Malnati, 2004-14 map <int, int> m1; map <int, int> :: iterator m1_Iter; typedef pair <int, int> Int_Pair; m1.insert ( Int_Pair ( 1, 10 ) ); m1.insert ( Int_Pair ( 2, 20 ) ); m1.insert ( Int_Pair ( 3, 30 ) ); m1_cIter = m1.end( ); Programmazione di Sistema 87 Posso sapere se è vuota, svuotarla o sapere quanti elementi sono associati a una certa chiave. Operatori (4) empty – restituisce true se la mappa è vuota ¾ clear – cancella tutti gli elementi della mappa ¾ count – restituisce il numero di elementi la cui chiave è quella passata come parametro © C. Barberis, G. Malnati, 2004-14 ¾ map<int, int> m; map<int, int>::size_type i; typedef pair<int, int> Int_Pair; m1.insert(Int_Pair(3, 4)); i = m.count(3); // i = 1 Programmazione di Sistema 88 ... e tutta una serie di altre operazioni. Operatori (5) ¾ equal_range – restituisce una coppia di iteratori che identificano l’intervallo di elementi la cui chiave è pari al valore specificato © C. Barberis, G. Malnati, 2004-14 typedef map <int, int, less<int> > IntMap; typedef pair <int, int> Int_Pair; m1.insert ( Int_Pair ( 1, 10 ) ); m1.insert ( Int_Pair ( 2, 20 ) ); m1.insert ( Int_Pair ( 3, 30 ) ); pair <IntMap::const_iterator, IntMap::const_iterator> p1, p2; p1 = m1.equal_range( 2 ); p1.first -> second; // vale 20 p1.second -> second; // vale 30 Programmazione di Sistema 89 Operatori (6) ¾ erase – rimuove uno o piu' elementi dalla posizione specificata © C. Barberis, G. Malnati, 2004-14 Iter1 = ++m1.begin(); m1.erase(Iter1); max_size – restituisce la dimensione massima della mappa ¾ size – restituisce il numero di elementi della mappa ¾ Programmazione di Sistema 90 Operatori (7) ¾ find – restituisce un iteratore che punta all'elemento la cui chiave è quella passata come parametro © C. Barberis, G. Malnati, 2004-14 m1.insert ( Int_Pair ( 1, 10 ) ); m1.insert ( Int_Pair ( 2, 20 ) ); m1.insert ( Int_Pair ( 3, 30 ) ); map <int, int> :: const_iterator m1Iter = m1.find( 2 ); Programmazione di Sistema 91 L'accesso agli elementi possiamo farlo direttamente con le quadre. Operatori (8) ¾ operator[] – inserisce o modifica il valore di un elemento della mappa m1[ 1 ] = 10; © C. Barberis, G. Malnati, 2004-14 ¾ get_allocator – restituisce una copia dell'allocatore utilizzato per costruire la mappa map <int, int>::allocator_type m1_Alloc; map <int, int, allocator<int> > m1; m1_Alloc = m1.get_allocator( ); Programmazione di Sistema 92 Operatori (9) lower_bound – restituisce un iteratore al primo elemento il cui valore della chiave è maggiore o uguale di quello specificato ¾ upper_bound – restituisce un iteratore al primo elemento il cui valore della chiave è maggiore di quello specificato ¾ swap – scambia gli elementi di due mappe © C. Barberis, G. Malnati, 2004-14 ¾ Programmazione di Sistema 93 Esempio d'uso di una mappa. © C. Barberis, G. Malnati, 2004-14 Esempio (1) struct ltstr { bool operator()(const char* s1, const char* s2) const { return strcmp(s1, s2) < 0; } }; int main() { map<const char*, int, ltstr> months; months["january"] = 31; months["february"] = 28; months["march"] = 31; months["april"] = 30; months["may"] = 31; months["june"] = 30; months["july"] = 31; months["august"] = 31; //… continua Programmazione di Sistema 94 Esempio (2) © C. Barberis, G. Malnati, 2004-14 months["september"] = 30; months["october"] = 31; months["november"] = 30; months["december"] = 31; cout << "june -> " << months["june"] << endl; map<const char*, int, ltstr>::iterator cur = months.find("june"); map<const char*, int, ltstr>::iterator prev = cur; map<const char*, int, ltstr>::iterator next = cur; ++next; --prev; cout << "Previous (in alphabetical order) is " << (*prev).first << endl; cout << "Next (in alphabetical order) is " << (*next).first << endl; } Programmazione di Sistema 95 /* Programmazione di sistema AA 2013-2014 - Lezione 15 */ /* (ripreso nella lezione 20) */ #include "stdafx.h" #include <list> #include <functional> #include <iostream> /* Unary function che ridefinisca una funzione che prende un parametro intero e restituisce come risultato un booleano: i due punti vogliono dire che f estende unary_function */ class f : public std::unary_function < int, bool > { int t; public: f() : t(0) {} bool operator() (int v) { t += v; if (t < 12) return false; else return true; } }; int _tmain(int argc, _TCHAR* argv[]) { std::list<int> l; l.push_back(1); l.push_back(2); l.push_back(4); l.push_back(8); l.push_back(16); std::cout << "Prima" << std::endl; for (std::list <int>::iterator it = l.begin(); it != l.end(); it++) { std::cout << *it << std::endl; } /* Qui vedo stampati 1 2 4 8 16 */ l.remove_if(f()); std::cout << "Dopo" << std::endl; for (std::list <int>::iterator it = l.begin(); it != l.end(); it++) { std::cout << *it << std::endl; } /* Qui vedo stampati 1 2 4 */ return 0; }
© Copyright 2025 ExpyDoc