¿Es una buena práctica confiar en que los encabezados se incluyan de forma transitiva?


37

Estoy limpiando las inclusiones en un proyecto de C ++ en el que estoy trabajando, y sigo preguntándome si debería incluir explícitamente todos los encabezados utilizados directamente en un archivo en particular, o si solo debería incluir lo mínimo.

Aquí hay un ejemplo Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Supongamos que una declaración directa para RenderObjectno es una opción).

Ahora, sé que eso RenderObject.hppincluye Texture.hpp, lo sé porque cada uno RenderObjecttiene un Texturemiembro. Aún así, incluyo explícitamente Texture.hppen Entity.hpp, porque no estoy seguro de si es una buena idea confiar en él se incluye en RenderObject.hpp.

Entonces: ¿Es una buena práctica o no?


19
¿Dónde están los guardias de inclusión en tu ejemplo? ¿Los olvidaste accidentalmente, espero?
Doc Brown

3
Un problema que ocurre cuando no incluye todos los archivos usados ​​es que a veces el orden en que los incluye se vuelve importante. Eso es realmente molesto solo en el caso en que sucede, pero a veces se acumulan bolas de nieve y terminas realmente deseando que la persona que escribió el código así se marche frente a un pelotón de fusilamiento.
Dunk

Por eso hay #ifndef _RENDER_H #define _RENDER_H ... #endif.
sampathsris

@ Dunk Creo que entendiste mal el problema. Con cualquiera de sus sugerencias, eso no debería suceder.
Mooing Duck

1
@DocBrown, lo #pragma onceresuelve, ¿no?
Pacerier

Respuestas:


65

Siempre debe incluir todos los encabezados que definan los objetos utilizados en un archivo .cpp en ese archivo, independientemente de lo que sepa sobre lo que hay en esos archivos. Debes incluir protectores en todos los archivos de encabezado para asegurarte de que incluir encabezados varias veces no importe.

Las razones:

  • Esto deja en claro a los desarrolladores que leen la fuente exactamente lo que requiere el archivo fuente en cuestión. Aquí, alguien que mira las primeras líneas del archivo puede ver que se trata de Textureobjetos en este archivo.
  • Esto evita problemas donde los encabezados refactorizados causan problemas de compilación cuando ya no requieren encabezados particulares. Por ejemplo, suponga que se da cuenta de que en RenderObject.hpprealidad no se necesita a Texture.hppsí mismo.

Un corolario es que nunca debe incluir un encabezado en otro encabezado a menos que sea explícitamente necesario en ese archivo.


10
De acuerdo con el corolario: ¡con la condición de que SIEMPRE debe incluir el otro encabezado si NO lo necesita!
Andrew

1
No me gusta la práctica de incluir directamente encabezados para todas las clases individuales. Estoy a favor de los encabezados acumulativos. Es decir, creo que el archivo de alto nivel debería hacer referencia a un "módulo" de algún tipo que esté utilizando, pero no necesita incluir directamente todas las partes individuales.
edA-qa mort-ora-y

8
Eso lleva a grandes encabezados monolíticos que cada archivo incluye, incluso cuando solo necesitan pequeños fragmentos de lo que contiene, lo que lleva a largos tiempos de compilación y dificulta la refactorización.
Gort the Robot

66
Google ha creado una herramienta para ayudarlo a aplicar precisamente este consejo, llamado incluir lo que usa .
Matthew G.

3
El problema principal del tiempo de compilación con grandes encabezados monolíticos no es el momento de compilar el código del encabezado en sí, sino tener que compilar cada archivo cpp en su aplicación cada vez que cambia el encabezado. Los encabezados precompilados no ayudan a eso.
Gort the Robot

23

La regla general es: incluye lo que usas. Si usa un objeto directamente, incluya su archivo de encabezado directamente. Si usa un objeto A que usa B pero no usa B usted mismo, solo incluya Ah

Además, mientras estamos en el tema, solo debe incluir otros archivos de encabezado en su archivo de encabezado si realmente lo necesita en el encabezado. Si solo lo necesita en el .cpp, solo inclúyalo allí: esta es la diferencia entre una dependencia pública y privada, y evitará que los usuarios de su clase arrastren encabezados que realmente no necesitan.


10

Me sigo preguntando si debo incluir explícitamente todos los encabezados utilizados directamente en un archivo en particular

Sí.

Nunca se sabe cuándo pueden cambiar esos otros encabezados. Tiene todo el sentido del mundo incluir, en cada unidad de traducción, los encabezados que sabe que necesita la unidad de traducción.

Tenemos protectores de encabezado para garantizar que la doble inclusión no sea perjudicial.


3

Las opiniones difieren en esto, pero soy de la opinión de que cada archivo (ya sea el archivo fuente c / cpp o el archivo de encabezado h / hpp) debería poder compilarse o analizarse por sí solo.

Como tal, todos los archivos deben # incluir todos y cada uno de los archivos de encabezado que necesitan; no debe suponer que un archivo de encabezado ya se ha incluido anteriormente.

Es un verdadero problema si necesita agregar un archivo de encabezado y descubrir que usa un elemento definido en otro lugar, sin incluirlo directamente ... así que debe buscar (¡y posiblemente terminar con el incorrecto!)

Por otro lado, no importa (como regla general) si #incluye un archivo que no necesita ...


Como punto de estilo personal, organizo los archivos #include en orden alfabético, divididos en sistema y aplicación, esto ayuda a reforzar el mensaje "autónomo y totalmente coherente".


Nota sobre el orden de incluye: a veces el orden es importante, por ejemplo cuando se incluyen encabezados X11. Esto puede deberse al diseño (que en ese caso podría considerarse un mal diseño), a veces se debe a desafortunados problemas de incompatibilidad.
hyde

Una nota sobre la inclusión de encabezados innecesarios, es importante para los tiempos de compilación, primero directamente (especialmente si es C ++ con plantillas pesadas), pero especialmente cuando se incluyen encabezados del mismo proyecto o de dependencia donde el archivo de inclusión también cambia y desencadenará la recompilación de todo incluido (si tiene dependencias funcionales, si no las tiene, entonces tiene que estar haciendo una compilación limpia todo el tiempo ...).
hyde

2

Depende de si esa inclusión transitiva es por necesidad (por ejemplo, clase base) o debido a un detalle de implementación (miembro privado).

Para aclarar, la inclusión transitiva es necesaria cuando la eliminación solo se puede hacer después de cambiar primero las interfaces declaradas en el encabezado intermedio. Dado que ya es un cambio importante, cualquier archivo .cpp que lo use debe verificarse de todos modos.

Ejemplo: Ah está incluido por Bh, que es utilizado por C.cpp. Si Bh usó Ah para algunos detalles de implementación, entonces C.cpp no ​​debería suponer que Bh continuará haciéndolo. Pero si Bh usa Ah para una clase base, entonces C.cpp puede suponer que Bh continuará incluyendo los encabezados relevantes para sus clases base.

Usted ve aquí la ventaja real de NO duplicar las inclusiones de encabezado. Digamos que la clase base utilizada por Bh realmente no pertenecía a Ah y se refactoriza en Bh. Bh ahora es un encabezado independiente. Si C.cpp incluye redundantemente Ah, ahora incluye un encabezado innecesario.


2

Puede haber otro caso: tienes Ah, Bh y tu C.cpp, Bh incluye Ah

entonces en C.cpp, puedes escribir

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Entonces, si no escribe #include "Ah" aquí, ¿qué puede pasar? en su C.cpp, se utilizan tanto A como B (por ejemplo, clase). Más tarde, cambió su código C.cpp, eliminó las cosas relacionadas con B, pero dejó Bh incluido allí.

Si incluye tanto Ah como Bh y ahora en este punto, las herramientas que detectan inclusiones innecesarias pueden ayudarlo a señalar que Bh include ya no es necesario. Si solo incluye Bh como se indicó anteriormente, es difícil para las herramientas / humanos detectar la inclusión innecesaria después del cambio de código.


1

Estoy adoptando un enfoque similar ligeramente diferente de las respuestas propuestas.

En los encabezados, siempre incluya solo un mínimo, solo lo que se necesita para que la compilación pase. Utilice la declaración hacia adelante siempre que sea posible.

En los archivos de origen, no es tan importante cuánto incluya. Mis preferencias todavía incluyen un mínimo para que pase.

Para proyectos pequeños, incluidos los encabezados aquí y allá, no habrá diferencia. Pero para proyectos medianos a grandes, puede convertirse en un problema. Incluso si se utiliza el último hardware para compilar, la diferencia puede ser notable. La razón es que el compilador todavía tiene que abrir el encabezado incluido y analizarlo. Por lo tanto, para optimizar la compilación, aplique la técnica anterior (incluya un mínimo, y use la declaración directa).

Aunque un poco anticuado, el diseño de software C ++ a gran escala (por John Lakos) explica todo esto en detalle.


1
No está de acuerdo con esta estrategia ... si incluye un archivo de encabezado en un archivo fuente, entonces tiene que rastrear todas sus dependencias. ¡Es mejor incluir directamente, que intentar documentar la lista!
Andrew

@ Andrew hay herramientas y scripts para verificar qué y cuántas veces se incluye.
B 7овић

1
He notado la optimización en algunos de los últimos compiladores para lidiar con esto. Reconocen una típica declaración de guardia y la procesan. Luego, al #incluirlo nuevamente, pueden optimizar la carga del archivo por completo. Sin embargo, su recomendación de declaraciones futuras es muy sabia para reducir el número de inclusiones. Una vez que comience a utilizar declaraciones directas, se convierte en un equilibrio entre el tiempo de ejecución del compilador (mejorado mediante declaraciones directas) y la facilidad de uso (mejorada por #incluidos adicionales convenientes), que es un equilibrio que cada compañía establece de manera diferente.
Cort Ammon

1
@CortAmmon Un encabezado típico incluye protectores, pero el compilador todavía tiene que abrirlo, y eso es una operación lenta
B 10овић

44
@ BЈовић: En realidad, no lo hacen. Todo lo que tienen que hacer es reconocer que el archivo tiene protectores de encabezado "típicos" y marcarlos para que solo se abra una vez. Gcc, por ejemplo, tiene documentación sobre cuándo y dónde aplica esta optimización: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon

-4

Una buena práctica es no preocuparse por su estrategia de encabezado siempre que se compile.

La sección de encabezado de su código es solo un bloque de líneas que nadie debería mirar hasta que obtenga un error de compilación fácil de resolver. Entiendo el deseo de un estilo "correcto", pero ninguna de las dos formas puede describirse realmente como correcta. La inclusión de un encabezado para cada clase es más probable que cause errores de compilación molestos basados ​​en el orden, pero esos errores de compilación también reflejan problemas que la codificación cuidadosa podría solucionar (aunque posiblemente no valga la pena el tiempo para solucionarlo).

Y sí, tendrá esos problemas basados ​​en pedidos una vez que comience a entrarfriend tierra.

Puedes pensar en el problema en dos casos.


Caso 1: tiene un pequeño número de clases que interactúan entre sí, digamos menos de una docena. Regularmente agrega, elimina y modifica de otra manera estos encabezados, de manera que pueden afectar sus dependencias entre sí. Este es el caso que sugiere su ejemplo de código.

El conjunto de encabezados es lo suficientemente pequeño como para que no sea complicado resolver los problemas que surjan. Cualquier problema difícil se soluciona reescribiendo uno o dos encabezados. Preocuparse por su estrategia de encabezado es resolver problemas que no existen.


Caso 2: Tienes docenas de clases. Algunas de las clases representan la columna vertebral de su programa, y ​​reescribir sus encabezados lo obligaría a reescribir / recompilar una gran cantidad de su código base. Otras clases usan esta columna vertebral para lograr cosas. Esto representa un entorno empresarial típico. Los encabezados se extienden por los directorios y no puede recordar de manera realista los nombres de todo.

Solución: en este punto, debe pensar en sus clases en grupos lógicos y reunir esos grupos en encabezados que eviten que tenga que #includerepetir una y otra vez. Esto no solo simplifica la vida, sino que también es un paso necesario para aprovechar los encabezados precompilados .

Se termina #includeing clases que no necesita, pero a quién le importa ?

En este caso, su código se vería como ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}

13
Tuve que -1 esto porque honestamente creo que cualquier oración que tenga la forma de "Una buena práctica es no preocuparse por su estrategia ____ mientras se compile" lleva a las personas a un mal juicio. He descubierto que el enfoque conduce muy rápidamente hacia la imposibilidad de leer, y la imposibilidad de leer es casi tan malo como "no funciona". También he encontrado muchas bibliotecas importantes que no están de acuerdo con los resultados de los dos casos que usted describe. Como ejemplo, Boost hace los encabezados de "colecciones" que recomienda en el caso 2, pero también hacen un gran problema al proporcionar encabezados de clase por clase para cuando los necesite.
Cort Ammon

3
Personalmente he presenciado que "no te preocupes si se compila" se convierte en "nuestra aplicación tarda 30 minutos en compilarse cuando agregas un valor a una enumeración, ¿cómo diablos arreglamos eso?"
Gort the Robot

Abordé el tema del tiempo de compilación en mi respuesta. De hecho, mi respuesta es una de las dos únicas (ninguna de las cuales obtuvo buenos resultados) que sí. Pero realmente, eso es tangencial a la pregunta de OP; este es un "¿Debería poner en camello mis nombres de variables?" tipo de pregunta Me doy cuenta de que mi respuesta es impopular, pero no siempre hay una mejor práctica para todo, y este es uno de esos casos.
QuestionC

De acuerdo con el n. ° 2. En cuanto a las ideas anteriores, espero una automatización que actualice el bloque de encabezado local, hasta entonces defiendo una lista completa.
chux - Restablece a Mónica el

El enfoque de "incluir todo y el fregadero de la cocina" puede ahorrarle algo de tiempo inicialmente; sus archivos de encabezado pueden incluso parecer más pequeños (ya que la mayoría de las cosas se incluyen indirectamente desde ... en algún lugar). Hasta que llegue al punto en que cualquier cambio en cualquier lugar provoque una finalización de su proyecto de más de 30 minutos. Y su autocompletado IDE-smart trae cientos de sugerencias irrelevantes. Y accidentalmente mezclas dos clases con nombres muy similares o funciones estáticas. Y agrega una nueva estructura, pero la compilación falla porque tiene una colisión de espacio de nombres con una clase totalmente no relacionada en alguna parte ...
CharonX
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.