Cosas para nunca hacer con C++ Builder
Sinceramente, estoy sorprendido. Los ejemplos que he estado mirando de C++ Builder en estos días, tanto en los manuales de Borland/Inprise como en libros que hay por ahí, son como para no poder dormir. ¿El motivo? La falta de atención que le prestan al tratamiento de excepciones. Por ejemplo, este método pertenece a la famosa MASTAPP, versión C++:
void TMastData::DeleteItems()
{
DeletingItems = True; // Suppress recalc of totals during delete
Items->DisableControls(); // for faster table traversal.
try
{
Items->First();
while(!Items->Eof)
Items->Delete();
}
catch(...)
{
DeletingItems = False;
Items->EnableControls(); //always re-enable controls
return;
}
DeletingItems = False;
Items->EnableControls(); //always re-enable controls
}
El código anterior tiene un error, y un mal aprovechamiento de recursos:
El error consiste en llamar a return dentro de catch. ¿Sabe lo que logra con esto? Pues que la excepción ha quedado "atrapada" y muere. El usuario nunca se entera de que ha sucedido un error. Pero no es esto lo peor. ¿Sabe usted desde dónde se va a llamar a DeleteItems? Yo no lo sé; puede que en algún caso particular usted lo sepa, pero es mala programación asumir estas dependencias dentro de un programa controlado por eventos. Un método manejador de eventos (no es éste el caso) tiene la siguiente particularidad: después de terminar la parte visible de su ejecución, el contador de instrucciones sigue moviéndose por el código interno de la VCL, y ahí usted no tiene control de todo lo que sucede. ¿Sabe el peligro a que se expone cuando deja que la VCL asuma que no hubo errores dentro de la respuesta al evento? La Tercera Regla de Marteens dice: "Nunca mates una excepción para la cual no tengas una solución".
El mal aprovechamiento de recursos se refiere a que las dos instrucciones finales se repiten, tanto en el catch como después del catch. Esto sucede porque C++ puro y duro no tiene la instrucción try/finally de Delphi y Java ... y de C sin los dos signos de adición. ¿Qué por qué me molesta la repetición de código? Lo de menos es que el programa sea un poco más grande. Lo de más es que cuando tengamos que modificar algo en esas dos instrucciones, tendremos que trabajar por duplicado ... y existe el riesgo de que ambos bloques pierdan la sincronía.
¿Qué hubiera hecho yo? Si me interesara ajustarme estrictamente a lo que permite el estándar de C++, hubiera sustituido, en primer lugar, el return por un throw (el raise de Delphi) para relanzar la excepción:
catch(...)
{
DeletingItems = False;
Items->EnableControls();
throw; // Esto es diferente
}
DeletingItems = False;
Items->EnableControls();
Pero no hubiera podido resolver la duplicación de código. Mi colega Dave viene utilizando desde hace tiempo la cláusula __finally de las excepciones estructuradas de C (no C++) y le ha funcionado de maravillas, sin problema alguno:
void TMastData::DeleteItems()
{
DeletingItems = True; // Suppress recalc of totals during delete
Items->DisableControls(); // for faster table traversal.
try
{
Items->First();
while(!Items->Eof)
Items->Delete();
}
__finally
{
DeletingItems = False;
Items->EnableControls(); //always re-enable controls
}
}
Lo que me preocupa es que éste no es el estilo empleado por la propia Inprise en sus manuales, pero repito: funciona sin problema alguno.
De todos modos, una de las causas más extendidas en Delphi y C++ Builder que hacen necesario el uso de try/finally es la creación de objetos dinámicos locales a un método. Una idea que se me ocurre es utilizar una técnica bastante extendida entre los programadores de C++: los punteros inteligentes, o smart pointers. He visto esta técnica en C++ Builder en relación con las interfaces para la programación COM, pero no he encontrado la correspondiente aplicación a la programación con la VCL día a día (si Borland la ha implementado o recomendado, debe haber escondido bastante bien la recomendación). Observe la siguiente plantilla de clase:
template<class T>
class LocalVCL
{
private:
T* Instance;
public:
LocalVCL(T* t) :
Instance(t) {}
LocalVCL()
{ Instance = new T(NULL); }
~LocalVCL()
{ delete Instance; }
T* operator->() { return Instance; }
operator T* () { return Instance; }
};
Esta plantilla define dos constructores: en uno se le pasa un puntero a un objeto arbitrario recién creado, y el otro no recibe nada, pero crea internamente un objeto. La clase con la que se instancia la plantilla debe permitir en este caso el uso de constructores con un solo parámetro: este el caso de cualquier componente de la VCL, que necesitan un owner o propietario. A este constructor se le pasa el puntero NULL, pues si se trata de un objeto de vida limitada, el propietario no tiene importancia alguna. En cualquier caso, el destructor destruye la instancia asignada durante la construcción. Recuerde que cuando definimos una variable de clase local a una rutina en C++, este lenguaje garantiza siempre su destrucción, aunque se produzcan excepciones. Es como si tuviéramos una instrucción try/finally implícita.
Para redondear la clase, definimos el operador ->, para que cuando lo apliquemos a una variable de tipo LocalVCL devuelva el objeto al cual apunta la instancia que contiene. El otro operador se encarga de las conversiones de tipo, desde un LocalVCL al componente de la VCL asociado. En todos los casos, las definiciones de los métodos son inline, para evitar código innecesario.
En el siguiente método vemos en acción a LocalVCL en dos situaciones diferentes: con un componente (el diálogo de configuración de la impresora) y con un objeto gráfico (un mapa de bits). En el caso del mapa de bits tenemos que utilizar el primer constructor, y construir explícitamente el objeto, ya que el constructor de la clase TBitmap no admite parámetros. En cambio, la construcción del diálogo es más sencilla y compacta, por tratarse de un componente:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
LocalVCL<Graphics::TBitmap> B = new Graphics::TBitmap;
LocalVCL<TPrinterSetupDialog> PS;
PS->Execute();
B->Width = Screen->Width;
B->Height = Screen->Height;
}
En ambos casos, el objeto creado se destruye inexorablemente al finalizar el método, aunque se produzca una excepción durante su ejecución, pues de ello se encarga el compilador. Si intentásemos utilizar el método correcto en C++ Builder que no utiliza smart pointers ni __finally, esto sería lo que obtendríamos:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Graphics::TBitmap* B = new Graphics::TBitmap;
try
{
TPrinterSetupDialog *PS = new TPrinterSetupDialog(NULL);
try
{
PS->Execute();
}
catch(...)
{
delete PS;
throw;
}
delete PS;
B->Width = Screen->Width;
B->Height = Screen->Height;
}
catch(...)
{
delete B;
throw;
}
delete B;
}
De todos modos, me gustaría escuchar opiniones diferentes a la mía.
POST SCRIPTUM
C++ Builder sigue manteniendo la definición de una "vieja conocida", la plantilla auto_ptr, que está definida en <memory>. A grandes rasgos, realiza la misma tarea que mi LocalVCL, pero soporta más operadores. El siguiente ejemplo muestra cómo puede utilizarse auto_ptr:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
// Observe que la sintaxis de la construcción es diferente
auto_ptr<Graphics::TBitmap> B(new Graphics::TBitmap);
auto_ptr<TPrinterSetupDialog> PS(NULL);
PS->Execute();
B->Width = Screen->Width;
B->Height = Screen->Height;
}
De todos modos, la diatriba anterior sigue estando justificada. Los ejemplos de programas con la VCL en C++ Builder de Inprise, y de la mayoría de los libros que andan por ahí no sólo carecen de la más mínima elegancia, sino que muchos de ellos son además incorrectos.
|