risorse | funzioni di lippincott
LE funzioni di Lippincott, così battezzate da Jon Kalb[3], catturano eccezioni C++ e le convertono in codici d'errore. Trovano uso nell'interfacciamento di codice C e C++, in particolare quando si vuole rendere disponibili in C delle funzionalità sviluppate in C++. La conversione è necessaria in quanto le eccezioni C++ innescano un undefined behaviour quando raggiungono il runtime del C:
…it's undefined behavior to let a C++ exception propagate through C code…[4]
…make sure you catch all possible exceptions in your C-callable C++ functions…[6]
…the C++ standard is somewhat vague about whether you can expect exceptions to behave properly…[7]
La modalità usuale con cui una funzionalità C++ che solleva delle eccezioni viene pubblicata in C è la seguente:
#include <cassert> #include <stdexcept> void f(int i) { if (i < 0) throw std::out_of_range("negative value"); else if (i % 2 == 1) throw std::invalid_argument("odd value"); // do something with i... } // step 1: explicit capture enum RESPONSE { OK, INVALID_ARGUMENT, OUT_OF_RANGE, UNKNOWN_ERROR, }; extern "C" RESPONSE f_(int i) { try { f(i); return OK; } catch (const std::invalid_argument&) { return INVALID_ARGUMENT; } catch (const std::out_of_range&) { return OUT_OF_RANGE; } catch (...) { return UNKNOWN_ERROR; } } int main() { assert(f_(42) == OK); assert(f_(-1) == OUT_OF_RANGE); assert(f_(11) == INVALID_ARGUMENT); return 0; }
Se le funzioni da pubblicare sono diverse, ognuna con la sua particolare lista di eccezioni da cattuare, è facile farsi “sfuggire” qualche tipo di eccezione, specie se il codice C++ è soggetto ad aggiornamenti, modifiche o refactoring:
// ... void g(double a, double b) { if (a > b) throw std::invalid_argument("a greater than b"); else if (a == 0) throw std::domain_error("a is zero"); // do something with a and b... } // ... enum RESPONSE { OK, INVALID_ARGUMENT, OUT_OF_RANGE, DOMAIN_ERROR, UNKNOWN_ERROR, }; extern "C" RESPONSE g_(double a, double b) { try { g(a, b); return OK; } catch (const std::invalid_argument&) { return INVALID_ARGUMENT; } catch (const std::domain_error&) { return DOMAIN_ERROR; } catch (...) { return UNKNOWN_ERROR; } } int main() { assert(f_(42) == OK); assert(f_(-1) == OUT_OF_RANGE); assert(f_(11) == INVALID_ARGUMENT); assert(g_(1, 1) == OK); assert(g_(2, 1) == INVALID_ARGUMENT); assert(g_(0, 1) == DOMAIN_ERROR); return 0; }
Conviene dunque centralizzare il codice di cattura e conversione delle eccezioni. Un modo per farlo è introducendo delle apposite macro:
// step 2: macro-based capture #define BEGIN_C_BOUNDARY \ try { #define END_C_BOUNDARY \ return OK; \ } \ catch (const std::invalid_argument&) { \ return INVALID_ARGUMENT; \ } catch (const std::out_of_range&) { \ return OUT_OF_RANGE; \ } catch (const std::domain_error&) { \ return DOMAIN_ERROR; \ } catch (...) { \ return UNKNOWN_ERROR; \ } extern "C" RESPONSE f_macro(int i) { BEGIN_C_BOUNDARY f(i); END_C_BOUNDARY } extern "C" RESPONSE g_macro(double a, double b) { BEGIN_C_BOUNDARY g(a, b); END_C_BOUNDARY } int main() { assert(f_(42) == OK); assert(f_(-1) == OUT_OF_RANGE); assert(f_(11) == INVALID_ARGUMENT); assert(g_(1, 1) == OK); assert(g_(2, 1) == INVALID_ARGUMENT); assert(g_(0, 1) == DOMAIN_ERROR); assert(f_macro(42) == OK); assert(f_macro(-1) == OUT_OF_RANGE); assert(f_macro(11) == INVALID_ARGUMENT); assert(g_macro(1, 1) == OK); assert(g_macro(2, 1) == INVALID_ARGUMENT); assert(g_macro(0, 1) == DOMAIN_ERROR); return 0; }
Un modo alternativo consiste nello sfruttare una funzione ausiliaria error_code_from_exception, la cosiddetta funzione di Lippincott:
// step 3. function-based capture RESPONSE error_code_from_exception() noexcept { try { throw; // re-throw the current exception } catch (const std::invalid_argument&) { return INVALID_ARGUMENT; } catch (const std::out_of_range&) { return OUT_OF_RANGE; } catch (const std::domain_error&) { return DOMAIN_ERROR; } catch (...) { return UNKNOWN_ERROR; } } extern "C" RESPONSE f_lippincott(int i) { try { f(i); return OK; } catch (...) { return error_code_from_exception(); } } extern "C" RESPONSE g_lippincott(double a, double b) { try { g(a, b); return OK; } catch (...) { return error_code_from_exception(); } } int main() { // ... assert(f_lippincott(42) == OK); assert(f_lippincott(-1) == OUT_OF_RANGE); assert(f_lippincott(11) == INVALID_ARGUMENT); assert(g_lippincott(1, 1) == OK); assert(g_lippincott(2, 1) == INVALID_ARGUMENT); assert(g_lippincott(0, 1) == DOMAIN_ERROR); return 0; }
error_code_from_exception è dichiarata noexcept per garantire che nessuna eccezione possa sfuggire alle funzioni *_lippincott. Se un'eccezione dovesse propagarsi al di fuori di error_code_from_exception, per effetto della dichiarazione noexcept il programma terminerebbe immediatamente — attraverso una chiamata a std::terminate(): in questo modo l'undefined behaviour è scongiurato.
Poiché error_code_from_exception forza il risollevamento dell'eccezione corrente, è necessario che tale eccezione esista; per questo motivo la funzione va richiamata esclusivamente all'interno di un blocco catch — una throw; al di fuori di un blocco catch causa una chiamata immediata a std::terminate().
#include <cassert> #include <iostream> #include <stdexcept> // ... int main() { // ... std::cout << "response: " << error_code_from_exception() << std::endl; // boom! return 0; }
Se si intende usare error_code_from_exception anche in altri ambiti, la si può rendere più sicura verificando esplicitamente l'esistenza dell'eccezione prima di risollevare l'eccezione:
RESPONSE error_code_from_exception() noexcept { try { if (std::current_exception()) throw; // re-throw the current exception else return OK; } catch (const std::invalid_argument&) { return INVALID_ARGUMENT; } catch (const std::out_of_range&) { return OUT_OF_RANGE; } catch (const std::domain_error&) { return DOMAIN_ERROR; } catch (...) { return UNKNOWN_ERROR; } } // ... /* output: * * response: 0 */
In [5] viene utilizzata una tecnica piuttosto interessante, che consiste nel passare alla funzione di cattura e conversione delle eccezioni anche il frammento di codice C++, sotto forma di funzione lambda. Il codice di interfacciamento diventa così più compatto:
template <typename F> RESPONSE lippincott(F&& f) noexcept { try { f(); return OK; } catch (...) { return error_code_from_exception(); } } extern "C" RESPONSE f_lippincott_lambda(int i) { return lippincott([&]{ f(i); }); } extern "C" RESPONSE g_lippincott_lambda(double a, double b) { return lippincott([&]{ g(a, b); }); } // ... int main() { // ... assert(f_lippincott_lambda(42) == OK); assert(f_lippincott_lambda(-1) == OUT_OF_RANGE); assert(f_lippincott_lambda(11) == INVALID_ARGUMENT); assert(g_lippincott_lambda(1, 1) == OK); assert(g_lippincott_lambda(2, 1) == INVALID_ARGUMENT); assert(g_lippincott_lambda(0, 1) == DOMAIN_ERROR); return 0; }
La forma generale della barriera C++/C diventa quindi:
extern "C" RESPONSE fun_name(args) { return lippincott([&]{ // c++ code that may throw }); }
Nel suo intervento dal titolo “Error Handling in C++” — successivamente ribattezzato “Declarative Control Flow” — all'NDC di Oslo del 2014, Alexandrescu fa uso di un oggetto temporaneo e di una particolare forma dell'operator+ per specificare una funzione lambda riducendo al minimo il carico sintattico:
// original code // ... return lippincott([&]{ // c++ code that may throw }); // auxiliary object with a custom operator+ that // takes a callable and wraps it with a `lippincott` call struct ErrorCodeFromException { template <typename F> RESPONSE operator+(F&& f) { return lippincott(f); } }; // modified code that instantiates a temporary ErrorCodeFromException // and passes the C++ code to wrap through a call to the operator+ method // ... return ErrorCodeFromException() + [&]{ // c++ code that may throw };
L'uso dell'operator+ (o di un qualunque altro operatore binario) consente di eliminare la coppia di parentesi tonde. L'istanziazione dell'oggetto temporaneo e il preambolo della dichiarazione lambda (e, a seconda dei casi, pure il return) possono a questo punto essere nascosti all'interno di una macro:
#define ERROR_CODE_FROM_EXCEPTION \ ErrorCodeFromException() + [&] // final code // ... return ERROR_CODE_FROM_EXCEPTION { // c++ code that may throw };
Il codice nella sua forma definitiva diventa quindi:
// ... extern "C" RESPONSE f_lippincott_final(int i) { return ERROR_CODE_FROM_EXCEPTION { f(i); }; } extern "C" RESPONSE g_lippincott_final(double a, double b) { return ERROR_CODE_FROM_EXCEPTION { g(a, b); }; } int main() { // ... assert(f_lippincott_final(42) == OK); assert(f_lippincott_final(-1) == OUT_OF_RANGE); assert(f_lippincott_final(11) == INVALID_ARGUMENT); assert(g_lippincott_final(1, 1) == OK); assert(g_lippincott_final(2, 1) == INVALID_ARGUMENT); assert(g_lippincott_final(0, 1) == DOMAIN_ERROR); return 0; }
Pagina modificata il 26/08/2014