miércoles, febrero 20, 2008

Observaciones Cpp

Observaciones Cpp


Puede que lo que voy a contar sea conocido en gran medida, pero seré breve y rápido.

Si alguien quiere repasar o profundizar en algún punto, fenomenal.

Los ejemplos no se han compilado e intencionadamente omito cosas que pueden hacer que no compilen (seguro que es obvio y no aporta nada escribirlo lo que falta)




Herencia
En c++ es normal hacer muchas cosas sin nada de herencia
No conviene abusar de la herencia
Algunos entendidos de c++ dicen que no se debe utilizar la herencia.
Excepto en casos que deben estar muy justificados
La herencia se utiliza con varios fines
Clasificación (es un)
Polimorfismo en tiempo de ejecución
Extender algo que ya existe



Constructor
Curiosa notación
// C++
class A {
int val;

A(int _val) : val(_val) {}

};

// Java
class A {
int val;

A(int _val)
{
val = _val;
}

};
¿Porqué en c++ se utiliza esa notación con ':'?
Son muy puristas
Les preocupa mucho el rendimiento

En el ejemplo anterior no hay una diferencia muy relevante.

En java, cuando se instancia una clase (se crea un objeto), la memoria que ocupará está rellenada con ceros. De esta forma todo se queda inicializado con los valores por defecto (quizá es por razones de seguridad).

Si luego le das un valor inicial, estás inicializando dos veces, una para nada.
Cuando se inicializan muchos objetos así, puede tener impacto en el rendimiento.

Pero en C++, se utiliza esta notación, sobre todo, por ser puristas y claros.

Un ejemplo más largo...

// C++
class A {
int entero;

A(int _entero) : entero(_entero) {}

};


class B {
A val; // #1

B(A valorInic) // esto no compila
{
val = valorInic;
}

};

No compila porque primero se crea val de tipo A con el contructor vacío (linea #1) y luego se intenta copiar el valor de valorInic en val.
Pero A no tiene constructor vacío.

En C++ el código del constructor, se ejecuta siempre, después de que se haya construído el objeto (en Java hay excepciones poco claras en la sintáxis)


Otro tipo de ejemplo...
// C++
class Padre {
int val;

Padre(int _val) : val(_val) {}

};


class Hija : public Padre {

Hija(int _val) // esto no compila
{
val = _val;
}

};

class Hija2 : public Padre {

Hija2(int _val) : Padre(_val) {} // ahora sí

};

Hija tampoco compila porque la clase padre no tiene constructor vacío.
Con la notación ':' se le dice que tiene que hacer en la construcción, no después






Destructores virtuales
Los destructores de C++ sólo son virtuales si se indica explícitamente
Que no sean virtuales les da más rendimiento, pero en ocasiones es necesario que sean virtuales

Ejemplo:
class Fruta {

int* punteroInt;

Fruta() : punteroInt(new int) {};

~Fruta() { delete punteroInt; };

};

class Platano : public Fruta {

int* punteroIntDelPlatano;

Platano() : punteroIntDelPlatano(new int) {};

~Platano() { delete punteroIntDelPlatano; };

};

...

Fruta* f = new Platano();

delete f;
En este mega programa tenemos una fuga de memoria (memoria sin liberar)
El tema es que f es de tipo de Fruta aunque señale a un plátano (como un plátano es una fruta...)
El delete f, llama al destructor de Fruta, con lo que se borra de la memoria su punteroInt. NUNCA SE LLAMA AL DESTRUCTOR del plátano (y este no tiene oportunidad de liberar su puntero, pobre....)
Una opción es hacer... delete dynamic_cast(f); (pero hay que estar un poco enfermo
Es mala opción porque no siempre sabes que f tiene un plátano, podría ser una fresa, y probar con todas las frutas es mala solución.
Opción 2. Hacer virtual el destructor del padre. SÍÍÍÍ
Opción 3. Pensárselo dos veces antes de utilizar la herencia y polimorfismo en tiempo de ejecución.
IMPORTANTE. Este problema no afecta sólo a la memoria, afecta a todo recurso "no local". Por ejemplo, un semáforo, región crítica, conexión bbdd, etc...
LA OPCIÓN CORRECTA... RAII
Tratar de no liberar explícitamente recursos (de ningun tipo)
Esto en el caso de la memoria, se puede hacer con un CountPtr
http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization)
Conviene saberlo aunque es poco probable que os suceda. Un escenario posible es con la vcl (haciendo algún componente)





Operador asignación y constructor de copia
C++ es muy amable y propone siempre que puede constructores de copia y operadores de asignación.
Pero no siempre los que propone sirven y hacen lo que queremos.
Es muy importante, siempre preguntarse si el que crea nos sirve o no
Anotar con un comentario que se ha verificado es saludable
OJO con clases con recursos no locales, habitualmente memoria en forma de punteros o referencias
Una curiosidad importante
class Pr {

Pr(int i)...

};

Pr prueba = Pr(4);
Aquí parece que...
Se instancia prueba de tipo Pr con constructor vacío
Luego se instancia Pr con valor 4
Luego se llama al operador de asignación para copiar el segundo objeto en el primero

PUES NO
Esa línea es totalmente equivalente a la siguiente (más clara en lo que hace)
Pr prueba(4);



Clases índice en mapas
std::map
Muy útil para almacenar datos indexados por lo que queramos de forma segura (verificación en tiempo de compilación) y eficiente
En un mapa la clase índice debe tener definida una relación de orden extricta con el operador <. Vamos, que si definimos el operador < style="font-weight: bold;">iteradores y end()
Por ejemplo, en un mapa llamamos al método find(xxx)
miMapa.find(esto)
SIEMPRE comprobar que no vale miMapa.end() (que significa que no lo ha encontrado)
En la próxima especificación de C++ esto será mejor
En Scala, en tiempo de compilación se verifica que no te olvidas
Si no se hace esta verificación, el programa explotará en algunas ocasiones





Punteros y referencias
No utilizar punteros crudos ni aritmética de punteros
Las referencias son punteros que no lo parecen (indirección implícita) y que siempre tienen valor (siempre están inicializados) y no tienen aritmética de punteros
Punteros nunca, referencias sí, en muchos casos
Envíe "aunque parezca increíble estoy leyendo esto" al 555 55 55, participará en el sorteo de un concepto
Los punteros a punteros y punteros a referencias NOOOOOOOO, caca, caca




Fugas de memoria
Dicen que C y C++ padecen de esto, pero... no parece razonable para el caso de C++
Para los casos raros en los que es necesario un puntero (memoria de montículo o por polimorfismo en tiempo de ejecución), utiliza un CountPtr
El problema del CountPtr está en las referencias circulares, pero no es tan fácil diseñar algo con referencias circulares en C++, especialmente si no se abusan de los punteros.
Con muy poco cuidado, eso no pasará nunca




Multimétodos
C++ no tiene
Está de moda, pero no hay consenso y lo discuten mucho
Scala, que ha metido de todo, no los tiene. El autor inicialmente dijo que no se acordó, pero después no lo puso porque decía era mejor sin.
Hay discusiones sobre esto en internet
A pesar de ser un concepto sencillo, tarde bastante en enterarme
// sin multimétodos

class Grafo {
virtual Distancia(Punto punto)=0;
}


class Punto : public Grafo
{

Distancia (Punto punto);
}

class Linea : public Grafo
{

Distancia (Punto punto);
}


Grafo g1 = new Punto(100,200);
Grafo g2 = new Punto(200, 600);

g1.Distancia(Punto(120, 400));

Grafo g3(100, 250, 30);

g3.Distancia(Punto(120, 400));



// con multimétodos

class Grafo {
...
}


class Punto : public Grafo
{
...
}

class Linea : public Grafo
{
...
}


Distancia(Punto p1, Punto p2) ...
Distancia(Linea l1, Punto p1) ...
Distancia(Linea l1, Linea l2) ...


Grafo g1 = new Punto(100,200);
Grafo g2 = new Punto(200, 600);
Grafo g3(100, 250, 30);

Distancia(g1, g2);
Distancia(g3, g2);

Sin multimétodos, la llamada a Distancia del un objeto Grafo, se pasa en tiempo de ejecución al hijo que corresponda, un punto o una línea (polimorfismo en tiempo de ejecución, una de las claves fundamentales de la OOP)
Pero el segundo objeto sobre el que se realiza el cálculo (el parámetro), no puede utilizar el mismo truco
Con multimétodos, a pesar de que llamamos a Distancia con dos parámetros de tipo Grafo, y no existe una Distancia definida para dos Grafos, en tiempo de ejecución, verifica que uno de los parámetros concretamente es un punto y el otro una línea y llama a esa Distancia concreta (o la que corresponda)
Se parece al polimorfismo en tiempo de ejecución, pero no sólo para uno, sino para varios
Lo dicho, que hay una discusión sobre el tema que se puede ver en internet
No está en C++ ni estará en la próxima especificación



Herencia múltiple de implementación
No utilizar
Es cierto que en algunos casos puede ser muy interesante, pero es muy delicado
Ahora está más de moda (y parece mejor) los mixins




Conversión implícita
Es un recurso potente que debe utilizarse con cuidado
Se puede conseguir en varias direcciones
Un constructor con un sólo parámetro del tipo a convertir
Operadores en la clase que se convierte
Podéis ver un ejemplo en la clase Variant de jleTibco
En general no utilizar




NUNCA...
NUNCA Utilizaré aritmética de punteros
NUNCA Utilizaré punteros crudos
NUNCA Arrays al estilo C (que en el fondo son punteros con aritmética de punteros)
En su lugar std::vector
NUNCA Herencia múltiple de implementación
NUNCA Abusaré de la herencia
NUNCA Utilizaré dynamic_cast, const_cast, ... (RTTI)
NUNCA Utilizaré reinterpret_cast (nunca jamás)
NUNCA haré un casting al estilo C (nunca jamás)
NUNCA Haré piezas que requieran liberación explícita de recursos
NUNCA Haré programas con muchas hebras
NUNCA Utilizaré mecanismos de concurrencia de bajo nivel (regiones críticas, semáforos, monitores, etc...)
NUNCA utilizaré "using namespace"
NUNCA intentaré utilizar namespaces anidados
NUNCA diré en la definición de una función o método las excepciones que puede lanzar
NUNCA pondré un nombre a un namespace superior a 3 letras
NUNCA utilizaré macros
NUNCA pensaré que C++ se parece a C
NUNCA manejaré los string de C ni la entrada salida de C, ni lista de parámetros variables (C otra vez)
NUNCA utilizar malloc ni semejantes
NUNCA Pondré degradados de colores ;-P
(consejos de un pecador)

No hay comentarios: