2011-05-04 16 views
12

He visto muchas explicaciones sobre when to use forward declarations over including header files, pero pocas de ellas explican por qué es importante hacerlo. Algunas de las razones que he visto son los siguientes:¿Por qué incluir un archivo de encabezado es algo tan malo?

  • velocidad de compilación
  • reducir la complejidad de la gestión del fichero de cabecera
  • eliminación de dependencias cíclicas

proceder de un entorno .NET encuentro gestión de cabecera frustrante. Tengo esta sensación de que necesito dominar las declaraciones futuras, pero he estado abandonando hasta ahora.

¿Por qué no puede el compilador funcionar para mí y averiguar mis dependencias utilizando un mecanismo (incluye)?

¿Cómo las declaraciones de reenvío aceleran las compilaciones ya que en algún punto se necesitará compilar el objeto al que se hace referencia?

Puedo comprar el argumento para una complejidad reducida, pero ¿cuál sería un ejemplo práctico de esto?

+0

No, pero al revés es muy útil. Ocultas lo que no se requiere y muestras lo que se requiere. – DumbCoder

Respuesta

19

"para masterizar declaraciones adelante" no es un requisito, es una guía útil cuando sea posible.

Cuando se incluye un encabezado, y atrae más encabezados, y aún más, el compilador tiene que trabajar mucho para procesar un solo módulo de traducción.

Puede ver la cantidad de, por ejemplo, con gcc -E:

Una sola #include <iostream> da a mi g ++ 4.5.2 18.560 líneas adicionales de código para procesar.

A #include <boost/asio.hpp> agrega otras 74,906 líneas.

A #include <boost/spirit/include/qi.hpp> agrega 154,024 líneas, eso es más de 5 MB de código.

Esto se suma, especialmente si se incluye descuidadamente en algún archivo incluido en cada archivo de su proyecto.

A veces revisar el código anterior y eliminarlo innecesariamente, mejora la compilación dramáticamente solo por eso. Reemplazar incluye declaraciones adelante en los módulos de traducción donde solo se usan referencias o punteros a alguna clase, lo mejora aún más.

+0

Adivina que parece una buena razón en sí misma. Tendré que empezar a usar declaraciones avanzadas de manera más apropiada. – Jon

+0

@Jon También un C++ hace mucha menos magia que un compilador Java o C#. Cada unidad de compilación es distinta y debe construirse desde cero. Mire la salida del preproceso para obtener una comprensión clara de que todo tipo que no sea una primitiva de langue debe definirse en un archivo en el momento de la compilación. – rerun

0

Creo que las declaraciones avanzadas aceleran la compilación porque el archivo de encabezado SOLAMENTE se incluye donde realmente se usa. Esto reduce la necesidad de abrir y cerrar el archivo una vez. Tiene razón en que en algún punto el objeto al que se hace referencia deberá compilarse, pero si solo estoy usando un puntero a ese objeto en mi otro archivo .h, ¿por qué realmente lo incluyo? Si le digo al compilador que estoy usando un puntero a una clase, eso es todo lo que necesita (siempre que no llame a ningún método en esa clase)

Esto no es el final. Esos archivos .h incluyen otros archivos .h ... Por lo tanto, para un proyecto grande, abrir, leer y cerrar, todos los archivos .h que se incluyen de manera repetitiva pueden convertirse en una sobrecarga significativa. Incluso con las comprobaciones #IF, aún tiene que abrirlas y cerrarlas mucho.

Practicamos esto en mi fuente de empleo. Mi jefe me explicó esto de manera similar, pero estoy seguro de que su explicación fue más clara.

+1

SO firma automáticamente sus publicaciones. (Su SO Flair es su firma.) No es necesario incluir una firma en su texto. –

+0

@Paul OK, gracias. –

0

¿Cómo las declaraciones de reenvío aceleran las compilaciones ya que en algún punto se necesitará compilar el objeto al que se hace referencia?

Debido a que incluyen es un preprocesador cosa , lo que significa que se realiza a través de la fuerza bruta al analizar el archivo. Su objeto se compilará una vez (compilador) y luego se vinculará (vinculador) según corresponda más adelante.

En C/C++, cuando se compila, usted tiene que recordar que hay toda una cadena de herramientas implicadas (preprocesador, compilador, enlazador además construir herramientas de gestión como la marca o Visual Studio, etc ...)

3

Porque cuando estás haciendo algo como esto:

bar.h:

class Bar { 
    int foo(Foo &); 
} 

A continuación, el compilador no necesita saber cómo se define la estructura/clase Foo; así que importar el encabezado que define a Foo es inútil. Además, importar el encabezado que define Foo también podría necesitar importar el encabezado que define alguna otra clase que usa Foo; y esto podría significar importar el encabezado que define alguna otra clase, etc .... tortugas hasta el final.

Al final, el archivo que el compilador está trabajando en contra es casi como el resultado de copiar pegando todos los encabezados; por lo que será grande sin una buena razón, y cuando alguien hace un error tipográfico en un archivo de encabezado que no necesita (o importa, o algo por el estilo), entonces la compilación de su clase comienza a tomar demasiado tiempo (o falla) sin ninguna razón obvia).

Así que es bueno dar la menor información posible al compilador.

4

¿Por qué no puede el compilador funcionar para mí y averiguar mis dependencias utilizando un mecanismo (incluye)?

No puede porque, a diferencia de otros idiomas, C++ tiene una gramática ambigua:

int f(X); 

¿Es una declaración de la función o de una definición de variable? Para responder a esta pregunta, el compilador debe saber qué significa X, por lo que X debe declararse antes de esa línea.

+1

Punto inteligente (+1). –

+1

Además, #include no es un mecanismo del compilador C, sino del preprocesador C (que simplemente agrega elementos a su unidad de compilación); por lo que el compilador no puede gestionar dependencias, porque, bueno, no hay cosas como una 'dependencia' en C/C++. – phtrivier

0

El bien y el mal. La batalla continúa, pero ahora en el campo de batalla de los archivos de encabezado. Los archivos de encabezado son una necesidad y una característica del lenguaje, pero pueden crear una gran cantidad de sobrecarga innecesaria si se utilizan de forma no óptima, p. No utilizando adelante declaraciones etc.

¿Cómo adelante declaraciones aceleran compilaciones desde en algún momento el objeto referenciado tendrá que ser compilado ?

Puedo comprar el argumento para la complejidad reducida de , pero ¿cuál sería un ejemplo práctico de ?

Las declaraciones a futuro son malas. Mi experiencia es que muchos programadores de C++ no son conscientes del hecho de que no es necesario incluir ningún archivo de encabezado, a menos que realmente desee usar algún tipo de letra, p.necesita tener el tipo definido para que el compilador comprenda lo que quiere hacer. Es importante tratar de abstenerse de incluir archivos de encabezado en otros archivos de encabezado.

Sólo pasa alrededor de un puntero de una función a otra, sólo se requiere una declaración hacia adelante:

// someFile.h 
class CSomeClass; 
void SomeFunctionUsingSomeClass(CSomeClass* foo); 

Incluyendo someFile.h no se requiere para incluir el archivo de cabecera de CSomeClass, ya que están simplemente hacer unas apuntando a ella, no usando la clase. Esto significa que el compilador solo necesita analizar una línea (clase CSomeClass;) en lugar de un archivo de encabezado completo (que podría estar encadenado a otros archivos de encabezado, etc.).

Esto reduce el tiempo de compilación y el tiempo de enlace, y estamos hablando de grandes optimizaciones aquí si tiene muchos encabezados y muchas clases.

2

¿Cómo las declaraciones de reenvío aceleran las compilaciones ya que en algún momento el objeto al que se hace referencia deberá compilarse?

1) reduce/S de disco (menos archivos de abrir, menos veces)

2) reducción de uso de memoria/CPU mayoría de las traducciones sólo necesitan un nombre. si usa/asigna el objeto, necesitará su declaración.

esto es probablemente donde hará clic para usted: cada archivo que compila compila lo que es visible en su traducción.

un sistema mal mantenido terminará incluyendo una tonelada de cosas que no necesita, entonces esto se compila para cada archivo que ve. mediante el uso de reenvíos siempre que sea posible, puede eludir eso, y reducir significativamente la cantidad de veces que se debe compilar una interfaz pública (y todas sus dependencias incluidas).

es decir: el contenido del encabezado no se compilará una vez. se compilará una y otra vez. todo en esta traducción debe ser analizado, comprobado que es un programa válido, revisado por advertencias, optimizado, etc. muchas, muchas veces.

incluyendo perezosamente solo agrega un aumento significativo de disco/cpu/memoria, lo que se convierte en tiempos de compilación intolerables para usted, al tiempo que introduce dependencias significativas (en proyectos no triviales).

Puedo comprar el argumento para la complejidad reducida, pero ¿cuál sería un ejemplo práctico de esto?

innecesarios incluye introducir dependencias como efectos secundarios. cuando edite un include (necesario o no), entonces cada archivo que lo incluya debe ser recompilado (no trivial cuando cientos de miles de archivos deben ser innecesariamente abiertos y compilados).

Lakos escribió un buen libro que cubre esto en detalle:

http://www.amazon.com/Large-Scale-Software-Design-John-Lakos/dp/0201633620/ref=sr_1_1?ie=UTF8&s=books&qid=1304529571&sr=8-1

1

Usé declaraciones directas simplemente para reducir la cantidad de navegación entre los archivos fuente. p.ej.si el módulo X llama a un pegamento o función de interfaz F en el módulo Y, usar una declaración directa significa que escribir la función y la llamada solo se puede hacer visitando 2 lugares, Xc y Yc no son un problema cuando un IDE bueno ayuda navegas, pero tiendo a preferir la codificación de abajo hacia arriba creando código de trabajo y luego descubriendo cómo envolverlo en lugar de hacerlo a través de la especificación de la interfaz de arriba hacia abajo ... a medida que evolucionan las interfaces es útil no tener que escribirlas en su totalidad.

En C (o C++, menos clases) es posible mantener verdaderamente detalles privados estructura sólo por definirlas en los archivos de origen que los utilizan, y sólo la exposición de su declaración a la mundo exterior - un nivel de boxeo negro que requiere el desempeño -destruir virtuales en la forma C++/clases de hacer las cosas. También es posible evitar tener que prototipar cosas (visitando el encabezado) al listar 'de abajo hacia arriba' dentro de los archivos fuente (buena antigua palabra clave estática).

El dolor de administrar encabezados a veces puede revelar cuán modular es o no su programa; si es verdaderamente modular, la cantidad de encabezados que debe visitar y la cantidad de código & que las estructuras de datos declaradas deben minimizarse.

Trabajar en un gran proyecto con "todo incluido en todas partes" a través de encabezados precompilados no fomentará esta modularidad real.

dependencias de módulos pueden correlacionarse con el flujo de datos relacionados con problemas de rendimiento, es decir, ambos i-caché & d-problemas de caché. Si un programa involucra muchos módulos que se llaman entre sí & modificar datos en muchos lugares aleatorios, es probable que tenga poca coherencia de caché; el proceso de optimización de dicho programa a menudo implicará romper pases y agregar datos intermedios ... a menudo causando estragos muchos 'diagramas de clase'/'frameworks' (o al menos requieren la creación de muchas estructuras de datos intermedios). El uso de plantillas pesadas a menudo significa complejas estructuras de datos que destruyen cachés y persiguen punteros. En su estado optimizado, se reducirán las dependencias & persiguiendo el puntero.

Cuestiones relacionadas