Lo standard C++ include una libreria per la gestione delle operazioni di I/O che prende il nome di Standard I/O Streams Library. Le API della libreria si basano sul paradigma della programmazione ad oggetti e nelle funzionalità assolvono il ruolo di funzioni ereditate dalla specifica dello standard C come printf()
o scanf()
per l'I/O da riga di comando, e fprintf()
e fscanf()
per l'I/O da file.
Tuttavia, rispetto ad esse, la libreria standard di I/O di C++ implementa il controllo statico dei tipi per i dati in input/output, ed in generale applica politiche più sofisticate per la gestione degli errori in fase di input.
Il data model della libreria verte attorno al concetto di canale o flusso (stream), cioè un'entità che veicola dati in una direzione specifica, in o out, accessibile da qualsiasi contesto e caratterizzata da un'interfaccia uniforme rispetto alla tipologia di sorgente o destinazione, cioè un qualsiasi dispositivo di input/output come un file, una tastiera, una risorsa in memoria etc.
I/O da riga di comando
L'interafaccia da riga di comando (CLI, command line interface) è un paradigma di interazione usato per funzionalità di I/O di base. In questa lezione analizzeremo alcune API di cui abbiamo già fatto uso nelle lezioni precedenti tralasciando volutamente le dinamiche proprie del loro funzionamento.
Per l'implementazione di tale paradigma, lo standard C++ definisce implicitamente la tastiera e lo schermo come dispositivi di input e output predefiniti. Ad essi sono associati canali unidirezionali distinti per l'output (std::cout
, std::cerr
e std::clog
), e per l'input (std::cin
).
Gli stream sono quindi oggetti globali istanziati a tempo di esecuzione di cui possiamo servirci come interfaccia di I/O verso i dispositivi connessi al sistema che ospita la nostra applicazione.
Generare output su riga di comando
L'output su riga di comando è tipico di programmi con un livello di interazione limitato con l'utente. In questo contesto, i messaggi di output possono essere usati per notificare lo stato del programma, un errore, il risultato di un'operazione specifica o una richiesta di immissione di dati per proseguire l'elaborazione.
Lo standard C++ predispone più di un canale per generare output sulla riga di comando secondo modalità che variano in base al tipo di messaggio. Queste entità sono istanze globali della medesima classe std::ostream
, che si differenziano per la loro configurazione:
std::cout
: è il canale predefinito per l'output, usato per veicolare i messaggi informativi per l'utente. La specifica del linguaggio prevede che le operazioni di inserimento su questo canale siano bufferizzate per massimizzare il throughput generale dell'applicazione. La presenza di un buffer comporta una latenza tra l'immissione dei dati nel canale e la loro effettiva riproduzione a schermo, che è però perfettamente tollerabile nella maggior parte dei casi.std::err
: è il canale usato per emettere messaggi di errore. Vista la sua particolare funzione, esso non è provvisto di buffer. Ciò implica che l'emissione di un messaggio di errore avviene istantaneamente.std::log
: è un canale accessorio che veicola messaggi la cui rilevanza non è prioritaria per l'utente. La loro utilità consiste nel tenere traccia delle operazioni effettuate dal programma nel caso in cui si verifichino comportamenti inattesi o condizioni di errore. Esso è provvisto di un buffer, come il canale di output standard.
L'immissione di dati in uno qualunque di questi canali avviene mediante l'uso dell'operatore di inserimento <<()
, secondo la seguente sintassi:
std::cout << "Questo è un messaggio informativo";
std::cerr << "Questo è un messaggio d'errore";
std::clog << "Se tutto va bene, questo messaggio non interessa a nessuno...";
La classe std::ostream
include un overloading di tale operatore per ogni tipo base del linguaggio. Sfruttando questo meccanismo, è possibile estendere questa modalità di accesso ai canali di output anche ai tipi definiti dall'utente.
Il valore restituito dall'operatore di inserimento è un riferimento al canale stesso, pertanto è possibile concatenare molteplici operazioni di output come mostrato di seguito:
std::cout << "1 + 1 = " << 1 + 1;
// equivalente a
std::cout.operator<<("1 + 1 =").operator<<(1+1);
Leggere l'input da riga di comando
Lo standard C++ predispone l'istanza globale std::cin
della classe std::istream
per veicolare l'input da riga di comando a tempo di esecuzione. Analogamente a quanto avviene per la generazione di output, l'estrazione di dati dal canale di input avviene mediante l'uso dell'operatore di estrazione >>()
, secondo la seguente sintassi:
int a;
std::cin >> a;
La classe std::istream
include un overloading di tale operatore per ogni tipo base del linguaggio. Sfruttando questo meccanismo è possibile estendere l'estrazione dal canale di input anche ai tipi definiti dall'utente.
Il canale di std::cin
gestisce i dati in input mediante un buffer, per le medesime regioni di ottimizzazione. Lo spazio è usato come separatore per i vari elementi del buffer, e di fatto consente di concatenare molteplici estrazioni consecutive. Il tipo della variabile che riceve il dato estratto dall'input condiziona l'interpretazione dei dati letti dal buffer.
Nel listato seguente, all'utente è richiesto di inserire un numero ed una stringa di testo, in quest'ordine.
#include <iostream>
int main()
{
std::cout << "Inserisci un numero e del testo: ";
int a;
std::string b;
std::cin >> a >> b;
if (std::cin.good())
{
std::cout << "Hai inserito " << a << " e " << b;
}
else
{
std::cerr << "Errore!";
}
return 0;
}
Il metodo good()
della classe std::istream
consente di verificare che i dati immessi dall'utente siano stati correttamente interpretati rispettivamente come un numero intero ed una stringa di testo, ed in caso contrario viene emesso un messaggio di errore.
Il modo in cui vengono gestiti gli spazi in questo caso non consente però di inserire una stringa composta da più parole, ad esempio di Hello world! verrebbe catturata solo Hello.
Per ovviare a questo problema, si può usare la funzione getline()
, che consente di leggere tutti i dati inseriti nello stream fino alla pressione del tasto Enter, come mostrato nel listato seguente:
#include <iostream>
int main()
{
std::cout << "Inserisci testo con degli spazi: ";
std::string b;
getline(std::cin, b);
if (std::cin.good())
{
std::cout << "Hai inserito " << b;
}
else
{
std::cerr << "Errore!";
}
return 0;
}
Altre operazioni su stream: flush, sync e tie
La gestione di un buffer ha un impatto sulle modalità di interazione con l'esterno e determina una serie di effetti collaterali che possono deviare il comportamento effettivo della nostra applicazione da quello atteso, ad esempio per l'introduzione di latenze indesiderate.
Le classi che implementano i canali espongono quindi un'interfaccia di controllo per la gestione del buffer che si compone di alcune API specifiche:
flush()
è un metodo della classestd::ostream
usato per eseguire lo svuotamento di tutti i dati contenuti nel buffer di modo da forzare la generazione dell'output, che nel caso della riga di comando si traduce nella riproduzione di esso a schermo:std::cout << "1 + 1 = " << 1 + 1; std::cout.flush();
sync()
è un metodo della classestd::istream
che consente di sincronizzare il contenuto del buffer del canale di input con i dati prodotti dal dispositivo di input sorgente. Anche se l'effetto di questa sincronizzazione dipende dalla particolare piattaforma, nella maggior parte delle implementazioni ciò si traduce nel forzare il dispositivo di input a riversare nel buffer del canale tutti i dati pendenti;tie()
è una funzione membro della classestd::ios
che è la classe base di entrambestd::istream
estd::ostream
. Tramite il ricorso a questa API è possibile associare ad un canale (di input o di output) un canale di output predefinito di modo che che ogni operazione sul primo comporta prima l'esecuzione diflush()
sul secondo.
Come esempio praticostd::cerr
è legato astd::cout
, e ciò comporta che ogni operazione effettuata sul primo venga eseguita dopo aver riprodotto a schermo tutti i contenuti eventualmente presenti nel buffer distd::cout
. Questa procedura serve a preservare l'ordine di emissione dei messaggi nei due canali, poichè anche esso può essere rilevante ai fini della reportistica.
Allo stesso modo anchestd::cin
è legato astd::cout
, in questo modo si garantische che eventuali messaggi informativi riguardo le operazioni di input siano mostrati all'utente nel momento opportuno. Ad esempio, il messaggio Inserisci il tuo nome e password deve essere mostrato effettivamente prima di che il prompt richieda all'utente di inserire le sue credenziali, e non dopo.
Sebbene solo raramente sia necessario approcciarsi alla gestione del buffer dei canali di I/O, la conoscenza di queste API è utile per apprendere le dinamiche del funzionamento interno degli oggetti istanziati dalla libreria. Nelle lezioni seguenti analizzeremo le API della Standard I/O Streams Library deputate alla formattazione e manipolazione dei dati, l'interfacciamento con altri dispositivi di I/O e le analogie e differenze rispetto il modello dati usato nel linguaggio C per l'implementazione delle medesime funzionalità.