studio programacion para móviles libro edición desarrollo desarrollar curso aprende aplicaciones c++ forward-declaration

c++ - para - manual de programacion android pdf



¿Debería uno usar declaraciones avanzadas en lugar de incluir siempre que sea posible? (9)

¿Debería uno usar declaraciones avanzadas en lugar de incluir siempre que sea posible?

No, las declaraciones directas explícitas no deben considerarse una pauta general. Las declaraciones directas son esencialmente copias y pegadas, o un código mal escrito, que en caso de que encuentre un error en él, debe corregirse en todas partes donde se usan las declaraciones directas. Esto puede ser propenso a errores.

Para evitar desajustes entre las declaraciones de "reenvío" y sus definiciones, coloque las declaraciones en un archivo de encabezado e incluya ese archivo de encabezado tanto en los archivos fuente de definición como en los de declaración.

En este caso especial, sin embargo, donde solo una clase opaca es declarada hacia adelante, esta declaración forward puede estar bien, pero en general, "usar declaraciones forward en vez de includes siempre que sea posible", como dice el título de este hilo, puede ser bastante arriesgado.

A continuación, se incluyen algunos ejemplos de "riesgos invisibles" relacionados con declaraciones futuras (riesgos invisibles = desajustes de declaraciones que el compilador o el vinculador no detectan):

  • Las declaraciones explícitas de símbolos que representan datos pueden ser inseguras, ya que dichas declaraciones pueden requerir un conocimiento correcto de la huella (tamaño) del tipo de datos.

  • Las declaraciones explícitas de símbolos que representan funciones también pueden ser inseguras, como los tipos de parámetros y el número de parámetros.

El siguiente ejemplo ilustra esto, por ejemplo, dos declaraciones de datos peligrosas y una función:

Archivo ac:

#include <iostream> char data[128][1024]; extern "C" void function(short truncated, const char* forgotten) { std::cout << "truncated=" << std::hex << truncated << ", forgotten=/"" << forgotten << "/"/n"; }

Archivo bc:

#include <iostream> extern char data[1280][1024]; // 1st dimension one decade too large extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param int main() { function(0x1234abcd); // In worst case: - No crash! std::cout << "accessing data[1270][1023]/n"; return (int) data[1270][1023]; // In best case: - Boom !!!! }

Compilando el programa con g ++ 4.7.1:

> g++ -Wall -pedantic -ansi a.c b.c

Nota: peligro invisible, ya que g ++ no proporciona errores ni advertencias del compilador o engarzador
Nota: Omitir la extern "C" lleva a un error de enlace para la function() debido al cambio de nombre de C ++.

Ejecutando el programa:

> ./a.out truncated=abcd, forgotten="♀♥♂☺☻" accessing data[1270][1023] Segmentation fault

Cuando una declaración de clase utiliza otra clase solo como punteros, ¿tiene sentido utilizar una declaración de reenvío de clase en lugar de incluir el archivo de cabecera para evitar de manera preventiva problemas con las dependencias circulares? entonces, en lugar de tener:

//file C.h #include "A.h" #include "B.h" class C{ A* a; B b; ... };

haz esto en su lugar:

//file C.h #include "B.h" class A; class C{ A* a; B b; ... }; //file C.cpp #include "C.h" #include "A.h" ...

¿Hay alguna razón para no hacer esto siempre que sea posible?


¿Hay alguna razón para no hacer esto siempre que sea posible?

Absolutamente: rompe la encapsulación al requerir que el usuario de una clase o función conozca y duplique los detalles de implementación. Si esos detalles de implementación cambian, el código que reenvía puede romperse mientras que el código que se basa en el encabezado continuará funcionando.

Reenviar declarando una función:

  • requiere saber que está implementado como una función y no una instancia de un objeto functor estático o (¡jade!) una macro,

  • requiere duplicar los valores predeterminados para los parámetros predeterminados,

  • requiere conocer su nombre real y espacio de nombres, ya que puede ser una declaración de using que lo atraiga a otro espacio de nombres, tal vez bajo un alias, y

  • puede perder en la optimización en línea.

Si el código de consumo se basa en el encabezado, el proveedor de la función puede cambiar todos esos detalles de implementación sin romper el código.

Adelante declarando una clase:

  • requiere saber si se trata de una clase derivada y de la (s) clase (s) base (s) derivada (s),

  • requiere saber que es una clase y no solo un typedef o una instanciación particular de una plantilla de clase (o saber que es una plantilla de clase y obtener todos los parámetros de la plantilla y los valores predeterminados correctos),

  • requiere conocer el nombre verdadero y el espacio de nombres de la clase, ya que puede ser una declaración de using que lo atraiga a otro espacio de nombres, quizás bajo un alias, y

  • requiere conocer los atributos correctos (tal vez tiene requisitos especiales de alineación).

De nuevo, declarar hacia adelante rompe la encapsulación de estos detalles de implementación, lo que hace que su código sea más frágil.

Si necesita cortar las dependencias de encabezado para acelerar el tiempo de compilación, obtenga el proveedor de la clase / función / biblioteca para proporcionar un encabezado de declaraciones forward especial. La biblioteca estándar hace esto con <iosfwd> . Este modelo preserva la encapsulación de los detalles de implementación y le da al mantenedor de la biblioteca la capacidad de cambiar esos detalles de implementación sin romper el código, todo mientras reduce la carga en el compilador.

Otra opción es utilizar un modismo pimpl, que oculta los detalles de implementación aún mejor y acelera las compilaciones a costa de una sobrecarga de tiempo de ejecución pequeña.


¿Hay alguna razón para no hacer esto siempre que sea posible?

Conveniencia.

Si sabe de antemano que cualquier usuario de este archivo de encabezado necesariamente deberá incluir también la definición de A para hacer cualquier cosa (o quizás la mayoría de las veces). Entonces es conveniente incluirlo de una vez por todas.

Este es un tema bastante delicado, ya que un uso demasiado liberal de esta regla general arrojará un código casi incompilable. Tenga en cuenta que Boost aborda el problema de forma diferente al proporcionar encabezados de "conveniencia" específicos que combinan un par de funcionalidades cercanas.


¿Hay alguna razón para no hacer esto siempre que sea posible?

La única razón por la que pienso es guardar algo de tipeo.

Sin declaraciones directas puede incluir el archivo de encabezado solo una vez, pero no aconsejo hacerlo en proyectos grandes debido a las desventajas señaladas por otras personas.


¿Hay alguna razón para no hacer esto siempre que sea posible?

Sí - Rendimiento. Los objetos de clase se almacenan con sus miembros de datos juntos en la memoria. Cuando utiliza punteros, la memoria del objeto real apuntado se almacena en otro lugar en el montón, generalmente muy lejos. Esto significa que acceder a ese objeto provocará que se pierda la memoria caché y se vuelva a cargar. Esto puede marcar una gran diferencia en situaciones donde el rendimiento es crucial.

En mi PC, la función Faster () funciona aproximadamente 2000 veces más rápido que la función Slower ():

class SomeClass { public: void DoSomething() { val++; } private: int val; }; class UsesPointers { public: UsesPointers() {a = new SomeClass;} ~UsesPointers() {delete a; a = 0;} SomeClass * a; }; class NonPointers { public: SomeClass a; }; #define ARRAY_SIZE 100000 void Slower() { UsesPointers list[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { list[i].a->DoSomething(); } } void Faster() { NonPointers list[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { list[i].a.DoSomething(); } }

En partes de aplicaciones que son críticas para el rendimiento o cuando se trabaja en hardware que es especialmente propenso a problemas de coherencia del caché, el diseño y el uso de los datos pueden marcar una gran diferencia.

Esta es una buena presentación sobre el tema y otros factores de rendimiento: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf



El método de declaración directa casi siempre es mejor. (No puedo pensar en una situación en la que sea mejor incluir un archivo donde puedas usar una declaración directa, pero no voy a decir que siempre es mejor por las dudas).

No hay desventajas en las clases de declaración avanzada, pero puedo pensar en algunas desventajas para incluir encabezados innecesariamente:

  • Tiempo de compilación más largo, ya que todas las unidades de traducción, incluido Ch , también incluirán Ah , aunque es posible que no lo necesiten.

  • posiblemente incluyendo otros encabezados que no necesita indirectamente

  • contaminando la unidad de traducción con símbolos que no necesitas

  • es posible que necesite recompilar los archivos fuente que incluyen ese encabezado si cambia (@PeterWood)


Sí, usar declaraciones directas siempre es mejor.

Algunas de las ventajas que ofrecen son:

  • Tiempo de compilación reducido.
  • Ningún espacio de nombres contamina.
  • (En algunos casos) puede reducir el tamaño de los binarios generados.
  • El tiempo de recompilación se puede reducir significativamente.
  • Evitar el potencial choque de nombres de preprocesador.
  • Implementando Idioma PIMPL , proporcionando así un medio de ocultar la implementación desde la interfaz.

Sin embargo, Reenviar declarar una clase hace que esa clase en particular sea un tipo Incompleto y eso restringe severamente qué operaciones puede realizar en el tipo Incompleto.
No puede realizar ninguna operación que requiera que el compilador conozca el diseño de la clase.

Con el tipo Incompleto, puede:

  • Declare que un miembro es un puntero o una referencia al tipo incompleto.
  • Declarar funciones o métodos que acepta / devuelve tipos incompletos.
  • Defina funciones o métodos que acepte / devuelva punteros / referencias al tipo incompleto (pero sin utilizar sus miembros).

Con el tipo Incompleto no puedes:

  • Úselo como una clase base.
  • Úselo para declarar un miembro.
  • Defina funciones o métodos usando este tipo.

Un caso en el que no desea tener declaraciones forward es cuando ellos mismos son engañosos. Esto puede suceder si algunas de sus clases tienen plantillas, como en el siguiente ejemplo:

// Forward declarations template <typename A> class Frobnicator; template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer; // Alternative: more clear to the reader; more stable code #include "Gibberer.h" // Declare a function that does something with a pointer int do_stuff(Gibberer<int, float>*);

Las declaraciones a futuro son las mismas que las duplicaciones de código: si el código tiende a cambiar mucho, debe cambiarlo en 2 lugares o más cada vez, y eso no es bueno.