Exit: terminare un programma

5 febbraio 2018

Negli esempi visti finora abbiamo sempre fatto uso dell’istruzione return 0 all’interno della funzione main() per causare la terminazione del programma.

Nella maggior parte dei casi, ciò è sufficiente per l’espletamento delle procedure di terminazione del programma (deallocazione della memoria del programma, invocazione dei distruttori degli oggetti allocati a stack, oggetti statici, etc.). Inoltre, il valore di ritorno può essere utile per segnalare all’ambiente esterno lo stato di terminazione del programma, tipicamente 0 quando non si sono riscontrati errori durante la sua esecuzione, ed un valore diverso da 0 quando si vuole notificare uno stato diverso.

Tuttavia lo standard C++ mette a disposizione più di un API dedicata alla terminazione del programma, che trovano applicazione in casi particolari in cui è necessario definire i dettagli del comportamento dell’applicazione anche in fase di chiusura.

Nel seguito analizzeremo alcune di queste interfacce per la terminazione del programma e le differenze che esse comportano rispetto all’uso di return.

std::exit()

La funzione std::exit() fa parte delle API dello standard C++ ed è definita nello header file <cstdlib>, all’interno del namespace std.

Essa accetta come argomento un valore che verrà usato per determinare un codice di terminazione valido per la piattaforma in uso. Normalmente essa viene sempre eseguita, anche quando non è esplicitamente invocata nel programma, ed il valore del suo argomento è determinato da quello restituito da return, o 0 se tale istruzione è implicita.

La sua importanza risiede nell’effettuare alcune operazioni preliminari alla chiusura del programma:

  • la distruzione di tutte le variabile allocate staticamente (o globali);
  • la chiusura ed il flush degli stream;
  • la distruzione di file temporanei creati con std::tempfile.

Tuttavia, è possibile alterare questo comportamento di base invocandola esplicitamente come mostrato nell’esempio seguente:

#include <cstdlib>
#include <iostream>

int main() 
{
	std::cout << "Questa stringa è stampata a schermo" << std::endl;    
	std::exit(EXIT_SUCCESS);
	std::cout << "Questa stringa riga di codice non viene eseguita in toto" << std::endl;   
}   

EXIT_SUCCESS è una macro definita anch’essa nello header <cstdlib>, che restitisce un valore intero il cui risultato dipendente dalla piattaforma, in modo da uniformarsi alla convenzione usata per i codici di stato del programma. EXIT_FAILURE è la macro usata per segnalare una terminazione non prevista del programma.

Da un punto di vista applicativo, esiste una differenza fondamentale tra l’uso esplicito di std::exit() e quello di return che consiste nella distruzione delle variabili localmente definite.

Quando si usa return, infatti, l’uscita dall’ambito di visibilità della funzione main() innesca il meccanismo di unwinding dello stack che consiste nell’invocazione dei distruttori di ogni variabile localmente definita nel contesto dal quale si sta uscendo, cioè la funzione main().

Quando la funzione std::exit() è invocata esplicitamente il programma viene terminato prima che avvenga l’uscita dal contesto del main(), e ciò spiega perchè le variabili ivi definite non vengano deallocate come di consueto.

Se per le istanze in uso è previsto un codice di clean up nel distruttore della classe di appartenenza, questa differenza può essere considerevole. Questo è il motivo per cui, di norma, l’invocazione di std::exit() non è diretta ma è demandata al runtime di C++.

std::atexit()

La funzione std::atexit() consente di registrare una o più funzioni che devono essere invocate durante la terminazione del programma. Un esempio è dato nel listato seguente:

#include <cstdlib>
#include <iostream>

void handler1()
{
	std::cout << "Esecuzione delle operazioni di chiusura." << std::endl;
}

int main() 
{ 
	const int registered = std::atexit(handler1);
	
	std::cout << "Richiesta terminazione del programma." << std::endl;
	return 0;
}   

L’esecuzione di callback in fase di chiusura mediante l’uso di atexit() è di fatto un retaggio del linguaggio C. In C++, ed in generale nel contesto della programmazione orientata agli oggetti, l’uso dei distruttori è la scelta preferibile. Tuttavia, nel caso di applicazioni multiparadigma, questa API rappresenta un’alternativa valida per la gestione di operazioni di chiusura ed alcuni compilatori ne fanno addirittura uso per invocare i distruttori delle variabili globali alla terminazione del programma.

std::abort() e std::terminate()

Le funzioni std::abort() e std::terminate() vengono impiegate in caso di terminazione anomala del programma, a seguito ad esempio di un errore irreparabile. In questo caso, il programma viene chiuso senza effettuare le operazioni di clean up nè localmente nè a livello globale.

Non è presente un argomento per segnalare lo stato del programma all’uscita, poichè è la funzione stessa che determina il codice di errore appropriato in base alla piattaforma. Alcuni sistemi operativi, in seguito all’invocazione di std::abort() raccolgono le informazioni relative alla terminazione del programma e notificano all’utente un report sintetico o un core-dump (cioè la copia su disco di tutta la memoria allocata dal programma) al fine di indagare le cause della terminazione anomala.

Tuttavia, quando si invoca espliciatamente std::terminate() è possibile associare un callback da eseguire prima della chiusura mediante la funzione std::set_terminate(), la quale accetta come argomento un puntatore a funzione del tipo void(void).

Ciò garantisce la possibilità di rilasciare adeguatamente delle risorse (un file, un database etc.), senza lasciarle in uno stato inconsistente, anche se lo stato del programma è degenerato in una condizione di errore ingestibile.

Novità introdotte dallo standard C++11

Lo standard C++11 ha introdotto una serie di API specifiche per la terminazione del programma che integrano quelle originarie. Non le sostituiscono, nè le rendono obsolete anche se la difficoltà nel trovare dei nomi pregnanti ed esaustivi, che mettano in risalto le sottili differenze tra le une e le altre, genera un po’ di confusione tra le API ante e post C++11.

Le nuove API sono focalizzate sulla risoluzione di problematiche inerenti la multiprogrammazione, ed in particolare la necessità di prevenire interferenze tra la distruzione di variabili globali e la terminazione dei singoli thread lanciati dall’applicazione.

Il problema consiste nell’evitare che la terminazione normale del programma, ad esempio su input dell’utente, possa degenerare in una condizione di errore per il solo fatto di distruggere una variable globale prima che tutti i thread che fanno riferimento ad essa siano terminati a loro volta.

Idealmente questa condizione dovrebbe essere gestita notificando a tutti i thread la necessità di terminare il programma, aspettando poi che tutti terminino la loro esecuzione e poi procedendo all’invocazione dei distruttori delle variabili globali. Tuttavia nel mondo reale, ciò non sempre è possibile. Ad esempio alcuni thread possono essere impiegati in operazioni ad elevata latenza, e quindi non immediatamente recettivi del comando di terminazione. Oppure alcuni thread possono essere interdipendenti, al punto da richiedere un ordine specifico nella terminazione per prevenire condizioni di errore o stallo (dead-lock).

Insomma esistono dei casi in cui, indipendentemente dallo sforzo profuso per la gestione di un contesto multiprogrammato, è impossibile prevedere tutte le possibilità e trovare una soluzione lineare per la terminazione del programma. La soluzione migliore, o meglio l’unica soluzione, consiste quindi nell’evitare di invocare i distruttori delle variabli globali, di modo che tutti i riferimenti siano sempre validi.

A questo scopo è stata introdotta la funzione std::_Exit(), che a differenza di std::exit(), termina il programma senza distruggere le variabili globali, nè eseguire callback registrati con std::atexit() per i motivi di cui sopra.

Tuttavia, l’impossibilità di eseguire funzioni alla chiusura del programma rende di fatto l’uso di std::_Exit() una soluzione drastica e non applicabile in alcuni contesti. In ragione di ciò è stata introdotta, come controparte di exit(), la funzione std::quick_exit() che non effettua la distruzione delle variabili statiche, e al contempo consente di registrare delle funzioni da eseguire alla chiusura mediante l’uso di std::at_quick_exit().

Tutte le lezioni

1 ... 47 48 49 ... 74

Se vuoi aggiornamenti su Exit: terminare un programma inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Exit: terminare un programma

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento dei dati per attività di marketing