Programmazione generica e Librerie C++

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;
}