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