Spesso è necessario garantire a tutti i costi l’esecuzione di un certo frammento di codice entro lo scope di una funzione, ovvero prima che essa ritorni il controllo al chiamante. Supponiamo di doverci interfacciare con un’API legacy che presenta il classico pattern:
- InitializeAPI(…) // alloca risorse
- UseApi(…)
- …
- TerminateAPI(…) // libera risorse
Quindi per essere utilizzata, l’API ha prima bisogno di un’inizializzazione e poi alla fine di una terminazione. Una sequenza di questo tipo non è “sicura”:
// exception-unsafe
void IWillUseTheAPI()
{
InitializeAPI();
UseAPI();
// other stuff
TerminateAPI();
}
Cosa accadrebbe infatti se qualcosa (e.g. UseApi) sollevasse un’eccezione prima di arrivare alla chiamata di terminazione? Chiaramente, per com’è scritto il codice, la chiamata a TerminateAPI andrebbe persa causando potenziali problemi al resto del programma.
La soluzione classica a questo problema è di usare l’idioma RAII, ovvero creare un oggetto che inizializzi risorse in costruzione e le rilasci in distruzione; qualcosa del tipo:
class LibWrapper
{
public:
LibWrapper()
{
InitializeAPI();
}
~LibWrapper()
{
TerminateAPI();
}
Use()
{
UseAPI();
}
};
void IWillUseTheAPI()
{
LibWrapper wrapper; // Qui inizializza
wrapper.Use(); // throw? Ok, terminerò lo stesso
} // Termina qui
Il C++ garantisce che in caso di eccezione tutti gli oggetti costruiti sullo stack vengano distrutti (stack unwinding). Nel nostro caso l’istanza di LibWrapper è costruita sullo stack quindi abbiamo la garanzia di chiamare TerminateAPI nel suo distruttore non appena wrapper uscirà dallo scope (ovvero un attimo prima che IwillUseTheAPI ritorni).
La soluzione proposta è chiaramente poco generica. Possiamo allora sfruttare le novità del C++11 per fare ancora di meglio?
Sì. Possiamo implementare la versione più semplice di un DEFER, ovvero un’operazione che rimandi l’esecuzione di un certo codice al termine della funzione in cui compare, anche se viene sollevata un’eccezione. In pseudocodice, qualcosa del tipo:
void IWillUseTheAPI()
{
InitAPI();
DEFER(TerminateAPI()); // sarà eseguito alla fine
//... altre chiamate
}
Grazie alle lambda, possiamo passare codice scritto al volo e tenerlo da parte. Per essere sufficientemente generici, è possibile scrivere un wrapper che riceva in costruzione un qualsiasi oggetto che supporti l’operatore di chiamata a funzione (e.g. una lambda, un funtore, una std::function) e lo “esegua” in distruzione. Sintetizzando:
template <typename F>
struct finalizer
{
template<typename T>
finalizer(T&& f)
: m_f { forward<T>(f) },
m_dismiss { false }
{
}
~finalizer()
{
if (!m_dismiss)
m_f();
}
// paranoia (leggi più avanti)
finalizer(finalizer&& other)
: m_f { move(other.m_f) },
m_dismiss { other.m_dismiss }
{
other.m_dismiss = true; // altrimenti m_f() può essere eseguita due volte
}
// non fanno parte della semantica del DEFER
finalizer(const finalizer&) = delete;
finalizer& operator=(const finalizer&) = delete;
private:
F m_f;
bool m_dismiss;
};
template <typename F>
finalizer<F> defer(F&& f)
{
return finalizer<F> { std::forward<F>(f) };
}
Perché ho usato il flag m_dismiss e ho scritto il move constructor? Perché, nella chiamata a defer, non è garantito che i compilatori elidano la copia del finalizer – anche se molto probabilmente lo faranno. Chiaramente non è opportuno copiare un finalizer (tutti eseguono m_f?!) ma ci viene in aiuto la move semantics (la responsabilità di eseguire m_f viene trasferita all’ultimo finalizer).
Universal references (e.g. F&&) e forward servono ad evitare potenziali copie (ma è anche possibile passare tutto per valore e fare delle move). Il codice è semplice, non fa altro che tenere da parte una “funzione” (in realtà un oggetto che sa comportarsi come tale) e poi eseguirla in distruzione.
Come utilizzare tutto questo? Così:
void IWillUseTheAPI()
{
InitAPI();
auto defer_1 = defer([]{ TerminateAPI(); });
// ...
}
Non vi piace? Concordo! Usiamo una semplicissima macro per evitare di dare un nome ad un oggetto che non dovrà più essere utilizzato e per esplicitare la semantica di quello che stiamo facendo:
// generalmente tutti hanno queste (o di più generiche) due nella propria codebase 🙂
#define PASTE_STRING(arg1, arg2) DO_PASTE_STRING(arg1, arg2)
#define DO_PASTE_STRING(arg1, arg2) arg1 ## arg2
#define DEFER(...) auto PASTE_STRING(defer_, __LINE__) = defer(__VA_ARGS__)
// e.g. auto defer_13 = defer(...)
E finalmente:
void IWillUseTheAPI()
{
InitAPI();
DEFER([]{ TerminateAPI(); }); // sarà eseguito alla fine
//... altre chiamate
}
Questa è stata una delle primissime utility che ho inserito nella mia libreria di supporto! Poco tempo dopo ho scoperto che già ci aveva pensato Alexandrescu e consiglio a tutti la visione del video (specialmente la prima parte, molto più deep di quella sullo ScopeGuard – l’equivalente del nostro DEFER).
Ricapitolando:
- No ad una cattiva struttura del codice che può portare problemi di exception-safety,
- Sì all’utilizzo dell’idioma RAII,
- Sì++ se riusciamo ad essere non solo safe ma anche generici e flessibili. DEFER è solo un semplice esempio!
Bello avere a disposizione un sistema analogo al “finally” per garantire l’esecuzione del codice “cascasse il mondo”. Ringrazio Marco per la scoperta!
Ma con il wrapper “base” potrei nascondere i “dettagli tecnici”. Usando il wrapper non ho la preoccupazione di proteggere ogni uso della libreria con DEFER(…). Il codice/algoritmo sarebbe semplificato, senza queste “distrazioni”.
Quali sono le linee guida in questo caso?
Quando è meglio (o necessario) usare direttamente DEFER piuttosto che creare il wrapper?
L’esempio è volutamente semplice, infatti si è creata anche un’interessante discussione su linkedin. Nel caso di API hai ragione, un wrapper ad-hoc nasconderebbe i dettagli di init/terminate. Anche se ci sono casi in cui l’uso dell’API è confinato in un solo punto, qui forse il DEFER ti evita di dover scrivere un wrapper che useresti solo da una parte.
In generale, secondo me ci sono due utilizzi interessanti del DEFER:
1) quando vuoi eseguire del codice “semplice” alla fine di una funzione. Per semplice intendo che non faccia cose per cui sarebbe opportuno un oggetto a parte. In questo caso ricade l’esempio dell’API se l’utilizzo è confinato in un solo punto. Altri esempi sono logging (e.g. vuoi a tutti i costi loggare l’uscita da una funzione – anche in caso di eccezione) e profilazione.
2) quando vuoi scrivere codice prototipale. Riprendi l’esempio dell’API, magari stai scrivendo un codice che si interfaccia con una libreria esterna. Fai una prima prova usando DEFER, tutto funziona e non hai dovuto scrivere nessun wrapper. Poi ti rendi conto che userai l’API in un secondo punto. Allora introduci un oggetto più intelligente. In questo senso DEFER ti ha anche aiutato a fare una piccola scelta di design, senza però toglierti la possibilità di: (1) provare in fretta la struttura del tuo codice, (2) essere conformi agli idiomi del C++ (RAII). Chiaramente il rovescio della medaglia è lasciare il codice con due DEFER, che potrebbe non essere l’ideale 🙂 Ma qui si cade, poi, nel dover distinguere prototipazione e produzione, che spesso dipende dal proprio buon senso e da tanti altri fattori che farebbero andare troppo fuori tema questo commento!
Ciao,
anche io mi sono fatto un po’ “di ginnastica mentale” su quest’argomento, e volevo contribuire con un paio di osservazioni:
1) la prima volta che ho visto questo paradigma è stato quando studiai il linguaggio D (dietro cui c’è sempre Alexandrescu), e comunque anche la libreria boost offre alcune soluzioni analoghe (vedi scope_exit).
2) la seconda è più sostanziale: essendo chiamato all’interno di un distruttore, il codice passato al DEFER NON deve lanciare eccezioni. Il motivo è semplice: se la DEFER viene invocata durante lo stack unwinding dovuto ad un eccezione, ed il codice a sua volta lancia un’altra accezione, avremmo due eccezioni contemporaneamente; lo standard in questo caso richiederebbe l’invocazione di std::terminate().
My 2¢
Ciao Goffredo, grazie per il commento!
1) assolutamente sì! Io mi sono ispirato dal GO, visto in ufficio da un mio collega.
2) concordo, non l’ho scritto per semplicità ma hai fatto benissimo a scriverlo nei commenti.
Finalmente ho trovato il tempo per iscrivermi. Complimenti per il sito.
Oltre ai commenti che avevo fatto su linkedin, vorrei aggiungere una soluzione alternativa che fa uso delle librerie boost, ovvero di scope_exit, che va bene anche in C++98/03 e che, a mio avviso, resta leggibile anche in C++11, nonostante il supporto per le lambda-expressions:
Grazie Luca, sia per il feedback che per la soluzione alternativa!
Ciao Marco, mi sai dire se c’e’ la possibilità di rieditare la propria risposta, per esempio per correggere eventuali errori di grammatica? Non riesco a trovare il modo; comunque, nell’esempio sopra intendevo ovviamente “#include ” (spero il sito non interpreti quanto scritto fra virgolette come un tag HTML).
Mmm io riesco ad editare (ho un bottoncino “Modifica” proprio sotto il commento). Forse c’è qualche strano privilegio wordpress da cambiare, ci guardo, grazie per la segnalazione!
Per la questione dell’include, Crayon (il syntax highlighter usato per postare codice) non dovrebbe aver problemi con maggiori/minori e virgolette.