tipos - Transición del paradigma de manejo de errores C `goto` al paradigma de manejo de excepciones C++
try catch para que sirve (4)
En C, hay una expresión goto común utilizada para manejar los errores y salir de la limpieza de una función. He leído que el manejo de excepciones a través de los bloques try-catch es preferido en los programas orientados a objetos,
Eso no es cierto en absoluto para C ++.
Pero C ++ tiene destructores deterministas en lugar de bloques finally
(que se usan, por ejemplo, en Java), y eso es un elemento de cambio para el código de manejo de errores.
He leído que todo este código de limpieza en los bloques catch puede ser innecesario en C ++ debido a algo llamado RAII,
Sí, en C ++ usa "RAII". Que es un nombre pobre para un gran concepto. El nombre es pobre porque pone énfasis en la nialización (Adquisición de recursos es inicialización). Lo importante de RAII, por el contrario, radica en la destrucción . Como el destructor de un objeto local se ejecutará al final de un bloque, pase lo que pase, ya sean retornos anticipados o incluso excepciones, es el lugar perfecto para el código que libera recursos.
pero no estoy familiarizado con el concepto.
Bueno, para el comienzo, puedes comenzar con la definición de Wikipedia:
http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
O ir directamente al sitio web de Bjarne Stroustrup:
http://www.stroustrup.com/bs_faq2.html#finally
Estoy seguro de que estaremos más que felices de responder preguntas sobre aspectos particulares de la expresión idiomática o los problemas que encuentre al usarla :)
¿Es adecuada mi implementación, o hay una mejor manera de manejar los errores y salir limpiamente de una función en C ++?
Su implementación no es lo que uno esperaría de un buen código de C ++.
Aquí hay un ejemplo usando RAII. Utiliza excepciones para informar errores y destructores para realizar operaciones de limpieza.
#include <fstream>
#include <stdexcept>
#include <vector>
// C or low-level functions to be wrapped:
int blackbox();
void undo_blackbox();
// just to be able to compile this example:
FILE *fp;
// The only self-made RAII class we need for this example
struct Blackbox {
Blackbox() {
if (!blackbox()) {
throw std::runtime_error("blackbox failed");
}
}
// Destructor performs cleanup:
~Blackbox() {
undo_blackbox();
}
};
void foobar(void){
// std::ifstream is an implementation of the RAII idiom,
// because its destructor closes the file:
std::ifstream is("blah.txt");
if (!is) {
throw std::runtime_error("could not open blah.txt");
}
Blackbox local_blackbox;
// std::vector itself is an implementation of the RAII idiom,
// because its destructor frees any allocated data:
std::vector<unsigned long> data(42);
for(size_t i = 0; i < data.size(); i++){
char buffer[256] = "";
if(!fgets(buffer, sizeof(buffer), fp)){
throw std::runtime_error("fgets error");
}
data[i] = strtoul(buffer, NULL, 0);
}
for(size_t i = 0; i < (data.size()/2); i++){
printf("%lu/n", data[i] + data[i + (data.size()/2)]);
}
// nothing to do here - the destructors do all the work!
}
Por cierto, +1 por tratar de aprender un nuevo concepto en un nuevo idioma. ¡No es fácil cambiar tu mentalidad en un idioma diferente! :)
Soy un programador de C aprendiendo C ++. En C, hay una expresión goto
común utilizada para manejar errores y salir limpiamente de una función . He leído que el manejo de excepciones a través de try
- catch
blocks es preferido en los programas orientados a objetos, pero estoy teniendo problemas para implementar este paradigma en C ++.
Tomemos por ejemplo la siguiente función en C que utiliza el paradigma de manejo de errores goto
:
unsigned foobar(void){
FILE *fp = fopen("blah.txt", "r");
if(!fp){
goto exit_fopen;
}
/* the blackbox function performs various
* operations on, and otherwise modifies,
* the state of external data structures */
if(blackbox()){
goto exit_blackbox;
}
const size_t NUM_DATUM = 42;
unsigned long *data = malloc(NUM_DATUM*sizeof(*data));
if(!data){
goto exit_data;
}
for(size_t i = 0; i < NUM_DATUM; i++){
char buffer[256] = "";
if(!fgets(buffer, sizeof(buffer), fp)){
goto exit_read;
}
data[i] = strtoul(buffer, NULL, 0);
}
for(size_t i = 0; i < NUM_DATUM/2; i++){
printf("%lu/n", data[i] + data[i + NUM_DATUM/2]);
}
free(data)
/* the undo_blackbox function reverts the
* changes made by the blackbox function */
undo_blackbox();
fclose(fp);
return 0;
exit_read:
free(data);
exit_data:
undo_blackbox();
exit_blackbox:
fclose(fp);
exit_fopen:
return 1;
}
Traté de recrear la función en C ++ usando el paradigma de manejo de excepciones como tal:
unsigned foobar(){
ifstream fp ("blah.txt");
if(!fp.is_open()){
return 1;
}
try{
// the blackbox function performs various
// operations on, and otherwise modifies,
// the state of external data structures
blackbox();
}catch(...){
fp.close();
return 1;
}
const size_t NUM_DATUM = 42;
unsigned long *data;
try{
data = new unsigned long [NUM_DATUM];
}catch(...){
// the undo_blackbox function reverts the
// changes made by the blackbox function
undo_blackbox();
fp.close();
return 1;
}
for(size_t i = 0; i < NUM_DATUM; i++){
string buffer;
if(!getline(fp, buffer)){
delete[] data;
undo_blackbox();
fp.close();
return 1;
}
stringstream(buffer) >> data[i];
}
for(size_t i = 0; i < NUM_DATUM/2; i++){
cout << data[i] + data[i + NUM_DATUM/2] << endl;
}
delete[] data;
undo_blackbox();
fp.close();
return 0;
}
Siento que mi versión de C ++ no implementó correctamente el paradigma de manejo de excepciones; de hecho, la versión de C ++ parece aún menos legible y más propensa a errores debido a una acumulación de código de limpieza que se acumula en los bloques catch
medida que crece la función.
He leído que todo este código de limpieza en los bloques catch puede ser innecesario en C ++ debido a algo llamado RAII , pero no estoy familiarizado con el concepto. ¿Es adecuada mi implementación, o hay una mejor manera de manejar los errores y salir limpiamente de una función en C ++?
Déjame reescribir eso para ti usando lenguaje c ++ con explicaciones en línea con el código
// void return type, we may no guarantees about exceptions
// this function may throw
void foobar(){
// the blackbox function performs various
// operations on, and otherwise modifies,
// the state of external data structures
blackbox();
// scope exit will cleanup blackbox no matter what happens
// a scope exit like this one should always be used
// immediately after the resource that it is guarding is
// taken.
// but if you find yourself using this in multiple places
// wrapping blackbox in a dedicated wrapper is a good idea
BOOST_SCOPE_EXIT[]{
undo_blackbox();
}BOOST_SCOPE_EXIT_END
const size_t NUM_DATUM = 42;
// using a vector the data will always be freed
std::vector<unsigned long> data;
// prevent multiple allocations by reserving what we expect to use
data.reserve(NUM_DATUM);
unsigned long d;
size_t count = 0;
// never declare things before you''re just about to use them
// doing so means paying no cost for construction and
// destruction if something above fails
ifstream fp ("blah.txt");
// no need for a stringstream we can check to see if the
// file open succeeded and if the operation succeeded
// by just getting the truthy answer from the input operation
while(fp >> d && count < NUM_DATUM)
{
// places the item at the back of the vector directly
// this may also expand the vector but we have already
// reserved the space so that shouldn''t happen
data.emplace_back(d);
++count;
}
for(size_t i = 0; i < NUM_DATUM/2; i++){
cout << data[i] + data[i + NUM_DATUM/2] << endl;
}
}
La característica más poderosa de c ++ no son las clases, es el destructor. El destructor permite que se descarguen o liberen recursos o responsabilidades cuando se abandona el alcance. Esto significa que no tiene que volver a escribir el código de limpieza varias veces. Además, porque solo los objetos construidos pueden ser destruidos; si nunca llega a un artículo y, por lo tanto, nunca lo construye, no pagará ninguna penalización en caso de destrucción si sucede algo.
Si se encuentra a usted mismo repitiendo el código de limpieza, debería ser una indicación de que el código en cuestión no está aprovechando la potencia del destructor y RAII.
Sí, debe usar RAII (Adquisición de recursos es inicialización) siempre que sea posible. Conduce a un código que es fácil de leer y seguro.
La idea central es que adquiera recursos durante la inicialización de un objeto y configure el objeto para que libere correctamente los recursos en su destrucción. El punto vital por el que esto funciona es que los destructores se ejecutan normalmente cuando se sale del alcance a través de una excepción.
En su caso, ya hay RAII disponible y simplemente no lo está usando. std::ifstream
(supongo que eso es a lo que se refiere tu ifstream
) de hecho se cierra a la destrucción. Por lo tanto, todas las llamadas close()
in catch
se pueden omitir de manera segura y sucederán automáticamente, precisamente para lo que RAII es.
Para los data
, también debe usar un contenedor RAII. Hay dos disponibles: std::unique_ptr<unsigned long[]>
y std::vector<unsigned long>
. Ambos se ocupan de la desasignación de memoria en sus respectivos destructores.
Finalmente, para blackbox()
, puede crear un contenedor de RAII trivial usted mismo:
struct BlackBoxer
{
BlackBoxer()
{
blackbox();
}
~BlackBoxer()
{
undo_blackbox();
}
};
Cuando se reescribió con estos, su código sería mucho más simple:
unsigned foobar() {
ifstream fp ("blah.txt");
if(!fp.is_open()){
return 1;
}
try {
BlackBoxer b;
const size_t NUM_DATUM = 42;
std::vector<unsigned long> data[NUM_DATUM];
for(size_t i = 0; i < NUM_DATUM; i++){
string buffer;
if(!getline(fp, buffer)){
return 1;
}
stringstream(buffer) >> data[i];
}
for(size_t i = 0; i < NUM_DATUM/2; i++){
cout << data[i] + data[i + NUM_DATUM/2] << endl;
}
return 0;
} catch (...) {
return 1;
}
}
Además, observe que su función usa un valor de retorno para indicar el éxito o el fracaso. Esto puede ser lo que desee (si la falla es "normal" para esta función), o podría representar solo ir a mitad de camino (si también se supone que la falla es excepcional).
Si es el último, simplemente cambie la función a void
, elimine la construcción try
- catch
y arroje una excepción adecuada en lugar de return 1;
.
Finalmente, incluso si decide mantener el enfoque de valor de retorno (que es perfectamente válido), considere cambiar la función para devolver bool
, con true
significado de éxito. Es más idiomático.
El principio de RAII es que use un tipo de clase para administrar cualquier recurso que necesite limpieza después de su uso; esa limpieza es hecha por el destructor.
Esto significa que puede crear un administrador de RAII local, que limpiará automáticamente todo lo que esté administrando cuando salga del alcance, ya sea debido al flujo normal del programa o una excepción. Nunca debería haber necesidad de un bloque catch
para limpiar; solo cuando necesite manejar o reportar la excepción.
En tu caso, tienes tres recursos:
- El archivo
fp
.ifstream
ya es un tipo de RAII, así que simplemente elimina las llamadas redundantes afp.close()
y todo está bien. - Los
data
memoria asignados. Use una matriz local si se trata de un tamaño fijo pequeño (como este), ostd::vector
si se debe asignar dinámicamente; entonces deshazte de ladelete
. - El estado configurado por
blackbox
.
Puede escribir su propia envoltura RAII para la malarkey "caja negra":
struct blackbox_guard {
// Set up the state on construction
blackbox_guard() {blackbox();}
// Restore the state on destruction
~blackbox_guard() {undo_blackbox();}
// Prevent copying per the Rule of Three
blackbox_guard(blackbox_guard const &) = delete;
void operator=(blackbox_guard) = delete;
};
Ahora puede eliminar todo su código de manejo de errores; Indicaría falla a través de excepciones (ya sea arrojado, o permitido propagar) en lugar de un valor de retorno mágico, dando:
void foobar(){
ifstream fp ("blah.txt"); // No need to check now, the first read will fail if not open
blackbox_guard bb;
const size_t NUM_DATUM = 42;
unsigned long data[NUM_DATUM]; // or vector<unsigned long> data(NUM_DATUM);
for(size_t i = 0; i < NUM_DATUM; i++){
string buffer;
// You could avoid this check by setting the file to throw on error
// fp.exceptions(ios::badbit); or something like that before the loop
if(!getline(fp, buffer)){
throw std::runtime_error("Failed to read"); // or whatever
}
stringstream(buffer) >> data[i]; // or data[i] = stoul(buffer);
}
for(size_t i = 0; i < NUM_DATUM/2; i++){
cout << data[i] + data[i + NUM_DATUM/2] << endl;
}
}