c++ - otro - incluir header en c
Pros y contras de poner todo el código en los archivos de encabezado en C++ (17)
Puede estructurar un programa C ++ para que (casi) todo el código resida en los archivos de encabezado. En esencia, parece un programa C # o Java. Sin embargo, necesita al menos un archivo .cpp
para obtener todos los archivos de encabezado al compilar. Ahora sé que algunas personas detestarían absolutamente esta idea. Pero no he encontrado inconvenientes convincentes de hacer esto. Puedo enumerar algunas ventajas:
[1] Tiempos de compilación más rápidos. Todos los archivos de encabezado solo se analizan una vez, porque solo hay un archivo .cpp. Además, un archivo de encabezado no se puede incluir más de una vez, de lo contrario obtendrá un salto de compilación. Hay otras maneras de lograr compilaciones más rápidas cuando se utiliza el enfoque alternativo, pero esto es muy simple.
[2] Evita las dependencias circulares, al hacerlas absolutamente claras. Si ClassA
en ClassA.h
tiene una dependencia circular en ClassB
en ClassB.h
, tengo que poner una referencia hacia adelante y sobresale. (Tenga en cuenta que esto es diferente de C # y Java, donde el compilador resuelve automáticamente las dependencias circulares. Esto fomenta las malas prácticas de codificación de la OMI). De nuevo, puede evitar las dependencias circulares si su código estaba en archivos .cpp
, pero en un proyecto del mundo real, los archivos .cpp
tienden a incluir encabezados aleatorios hasta que no puede determinar quién depende de quién.
¿Tus pensamientos?
Bueno, como muchos han señalado, hay muchos inconvenientes en esta idea, pero para equilibrar un poco y proporcionar un profesional, diría que tener algún código de biblioteca en los encabezados tiene sentido, ya que lo hará independiente de otros ajustes en el proyecto en el que se usa.
Por ejemplo, si intenta utilizar diferentes bibliotecas de código abierto, puede configurarlas para que utilicen diferentes enfoques para vincularse a su programa: algunas pueden usar el código de biblioteca cargado dinámicamente del sistema operativo, otras están configuradas para estar vinculadas estáticamente; algunos pueden configurarse para usar subprocesamiento múltiple, mientras que otros no. Y puede ser una tarea abrumadora para un programador, especialmente con una restricción de tiempo, tratar de ordenar estos enfoques incompatibles.
Sin embargo, todo eso no es un problema cuando se usan bibliotecas que están contenidas por completo en los encabezados. "Simplemente funciona" para una biblioteca razonablemente bien escrita.
Creo que a menos que esté utilizando los encabezados precompilados de MSVC, y esté utilizando un Makefile u otro sistema de compilación basado en la dependencia, tener archivos de origen separados debería compilarse más rápido cuando se compile iterativamente. Dado que mi desarrollo es casi siempre iterativo, me preocupa más cuán rápido puede recompilar los cambios que hice en el archivo x.cpp que en los otros veinte archivos fuente que no cambié. Además, realizo cambios con mucha mayor frecuencia a los archivos fuente que a las API, por lo que cambian con menos frecuencia.
En cuanto a, dependencias circulares. Tomaría el consejo de paercebal un paso más allá. Tenía dos clases que tenían punteros el uno al otro. En cambio, me encuentro con el caso con más frecuencia donde una clase requiere otra clase. Cuando esto sucede, incluyo el archivo de encabezado para la dependencia en el archivo de encabezado de la otra clase. Un ejemplo:
// foo.hpp
#ifndef __FOO_HPP__
#define __FOO_HPP__
struct foo
{
int data ;
} ;
#endif // __FOO_HPP__
.
// bar.hpp
#ifndef __BAR_HPP__
#define __BAR_HPP__
#include "foo.hpp"
struct bar
{
foo f ;
void doSomethingWithFoo() ;
} ;
#endif // __BAR_HPP__
.
// bar.cpp
#include "bar.hpp"
void bar::doSomethingWithFoo()
{
// Initialize f
f.data = 0;
// etc.
}
La razón por la que incluyo esto, que no está relacionado con las dependencias circulares, es que creo que hay alternativas para incluir archivos de encabezado de cualquier manera. En este ejemplo, el archivo fuente de la barra de estructura no incluye el archivo de encabezado struct foo. Esto se hace en el archivo de encabezado. Esto tiene la ventaja de que un desarrollador que utiliza la barra no tiene que saber acerca de ningún otro archivo que el desarrollador deba incluir para usar ese archivo de encabezado.
Es posible que desee comprobar Lazy C ++ . Le permite colocar todo en un solo archivo y luego se ejecuta antes de la compilación y divide el código en archivos .h y .cpp. Esto podría ofrecerte lo mejor de ambos mundos.
Los tiempos de compilación lenta generalmente se deben a un acoplamiento excesivo dentro de un sistema escrito en C ++. Quizás necesite dividir el código en subsistemas con interfaces externas. Estos módulos podrían compilarse en proyectos separados. De esta forma puede minimizar la dependencia entre los diferentes módulos del sistema.
Estás fuera del alcance del diseño del idioma. Si bien puede tener algunos beneficios, eventualmente morderá en el trasero.
C ++ está diseñado para h archivos que tienen declaraciones y archivos cpp que tienen implementaciones. Los compiladores se basan en este diseño.
Sí , la gente debate si esa es una buena arquitectura, pero es el diseño. Es mejor dedicar su tiempo a su problema que reinventar nuevas formas de diseñar la arquitectura de archivos C ++.
La desventaja obvia para mí es que siempre tienes que construir todo el código a la vez. Con los archivos .cpp
, puede tener una compilación por separado, por lo que solo está reconstruyendo bits que realmente han cambiado.
Los kludges estáticos o de variable global son incluso menos transparentes, quizás no depurables.
por ejemplo, contar el número total de iteraciones para el análisis.
En MY kludge files, colocar dichos elementos en la parte superior del archivo cpp hace que sea más fácil encontrarlos.
Por "tal vez no depurable", quiero decir que rutinariamente pondré un global en la ventana de WATCH. Como siempre está en el alcance, la ventana de WATCH siempre puede acceder a ella sin importar dónde esté el contador del programa en este momento. Al colocar esas variables fuera de un {} en la parte superior de un archivo de encabezado, permitiría que todo el código descendente "las vea". Al ponerlos DENTRO de {}, de forma improvisada, creo que el depurador ya no los considerará "dentro del alcance" si su contador de programa está fuera del {}. Mientras que con kludge-global-at-Cpp-top, aunque puede ser global hasta el punto de mostrarse en su link-map-pdb-etc, sin una declaración externa, los otros archivos Cpp no pueden acceder a ella , evitando el acoplamiento accidental.
Me gusta pensar en la separación de archivos .h y .cpp en términos de interfaces e implementaciones. Los archivos .h contienen las descripciones de la interfaz en una clase más y los archivos .cpp contienen las implementaciones. A veces hay problemas prácticos o claridad que impiden una separación completamente limpia, pero ahí es donde comienzo. Por ejemplo, las funciones de acceso pequeño normalmente código en línea en la declaración de clase para mayor claridad. Las funciones más grandes están codificadas en el archivo .cpp
En cualquier caso, no permita que el tiempo de compilación dicte cómo estructurará su programa. Es mejor tener un programa que sea legible y mantenible en lugar de uno que compila en 1.5 minutos en vez de 2 minutos.
No estoy de acuerdo con el punto 1.
Sí, solo hay un .cpp y el tiempo creado desde cero es más rápido. Pero, rara vez construyes desde cero. Hace pequeños cambios, y tendría que volver a compilar todo el proyecto cada vez.
Prefiero hacerlo al revés:
- mantener declaraciones compartidas en archivos .h
- mantener definición para clases que solo se usan en un lugar en archivos .cpp
Por lo tanto, algunos de mis archivos .cpp comienzan a verse como código Java o C #;)
Pero, el enfoque ''mantener cosas en .h'' es bueno mientras diseñas el sistema, debido al punto 2. que hiciste. Normalmente hago eso mientras estoy construyendo la jerarquía de clases y más tarde cuando la arquitectura del código se vuelve estable, muevo el código a archivos .cpp.
Una cosa a la que estás renunciando a la que me costaría mucho vivir es el espacio de nombres anónimos.
Encuentro que son increíblemente valiosos para definir funciones de utilidad específicas de clase que deben ser invisibles fuera del archivo de implementación de la clase. También son geniales para almacenar datos globales que deberían ser invisibles para el resto del sistema, como una instancia de singleton.
Usted no comprende cómo se pretendía usar el idioma. Los archivos .cpp son realmente (o deberían ser, con la excepción del código en línea y de plantilla) los únicos módulos de código ejecutable que tiene en su sistema. Los archivos .cpp se compilan en archivos de objeto que luego se vinculan entre sí. Los archivos .h existen únicamente para la declaración directa del código implementado en archivos .cpp.
Esto da como resultado un tiempo de compilación más rápido y un ejecutable más pequeño. También se ve considerablemente más limpio porque puede obtener una visión general rápida de su clase mirando su declaración .h.
En cuanto al código en línea y de plantilla, ya que ambos se utilizan para generar código por el compilador y no por el vinculador, siempre deben estar disponibles para el compilador por archivo .cpp. Por lo tanto, la única solución es incluirlo en su archivo .h.
Sin embargo, he desarrollado una solución donde tengo mi declaración de clase en un archivo .h, toda la plantilla y el código en línea en un archivo .inl y toda la implementación de código no plantilla / en línea en mi archivo .cpp. El archivo .inl está #incluido en la parte inferior de mi archivo .h. Esto mantiene las cosas limpias y consistentes.
Tiene razón al decir que su solución funciona. Incluso puede no tener inconvenientes para su proyecto actual y entorno en desarrollo.
Pero...
Como otros dijeron, poner todo tu código en archivos de encabezado obliga a una compilación completa cada vez que cambias una línea de código. Esto puede no ser un problema todavía, pero su proyecto puede crecer lo suficiente como para que el momento de la compilación sea un problema.
Otro problema es cuando se comparte el código. Si bien es posible que aún no esté directamente interesado, es importante mantener oculto la mayor cantidad de código posible para un posible usuario de su código. Al poner su código en el archivo de encabezado, cualquier programador que use su código debe ver todo el código, mientras que simplemente está interesado en cómo usarlo. Al colocar su código en el archivo cpp, solo se puede entregar un componente binario (una biblioteca estática o dinámica) y su interfaz como archivos de encabezado, que pueden ser más simples en algún entorno.
Esto es un problema si desea poder convertir su código actual en una biblioteca dinámica. Como no tiene una declaración de interfaz adecuada desacoplada del código real, no podrá entregar una biblioteca dinámica compilada y su interfaz de uso como archivos de encabezado legibles.
Es posible que todavía no tenga estos problemas, por eso le dije que su solución puede estar bien en su entorno actual. Pero siempre es mejor estar preparado para cualquier cambio y algunos de estos problemas deberían abordarse.
PD: Acerca de C # o Java, debe tener en cuenta que estos idiomas no están haciendo lo que usted dice. En realidad, compilan archivos de manera independiente (como archivos cpp) y almacenan la interfaz de forma global para cada archivo. Estas interfaces (y cualquier otra interfaz vinculada) se utilizan para vincular todo el proyecto, es por eso que son capaces de manejar referencias circulares. Debido a que C ++ solo hace un pase de compilación por archivo, no puede almacenar interfaces de manera global. Es por eso que debe escribirlos explícitamente en archivos de encabezado.
Un problema con el código en los encabezados es que debe estar en línea, de lo contrario tendrá problemas de definición múltiple al vincular múltiples unidades de traducción que incluyan ese mismo encabezado.
La pregunta original especificaba que solo había una sola cpp en el proyecto, pero ese no es el caso si está creando un componente destinado a una biblioteca reutilizable.
Por lo tanto, en aras de crear el código más reutilizable y sostenible posible, solo coloque el código insertado e ineludible en los archivos de encabezado.
Una desventaja de su enfoque es que no puede hacer una compilación paralela. Puede pensar que está compilando más rápido ahora, pero si tiene varios archivos .cpp, puede compilarlos en paralelo en múltiples núcleos en su propia máquina o utilizando un sistema de compilación distribuida como distcc o Incredibuild.
Motivo [1] Tiempos de compilación más rápidos
No en mis proyectos: los archivos fuente (CPP) solo incluyen los encabezados (HPP) que necesitan. Entonces, cuando necesito recompilar solo un CPP debido a un pequeño cambio, tengo diez veces la misma cantidad de archivos que no se vuelven a compilar.
Quizás deba dividir su proyecto en fuentes / encabezados más lógicos: una modificación en la implementación de la clase A NO debería necesitar la recompilación de implementaciones de clase B, C, D, E, etc.
Motivo [2] Evita las dependencias circulares
Dependencias circulares en el código?
Lo siento, pero todavía no he tenido este tipo de problema siendo un problema real: digamos que A depende de B y B depende de A:
struct A
{
B * b ;
void doSomethingWithB() ;
} ;
struct B
{
A * a ;
void doSomethingWithA() ;
} ;
void A::doSomethingWithB() { /* etc. */ }
void B::doSomethingWithA() { /* etc. */ }
Una buena forma de resolver el problema sería dividir esta fuente en al menos una fuente / encabezado por clase (de una manera similar a la manera Java, pero con una fuente y un encabezado por clase):
// A.hpp
struct B ;
struct A
{
B * b ;
void doSomethingWithB() ;
} ;
.
// B.hpp
struct A ;
struct B
{
A * a ;
void doSomethingWithA() ;
} ;
.
// A.cpp
#include "A.hpp"
#include "B.hpp"
void A::doSomethingWithB() { /* etc. */ }
.
// B.cpp
#include "B.hpp"
#include "A.hpp"
void B::doSomethingWithA() { /* etc. */ }
Por lo tanto, no hay problema de dependencia, y aún tiempos de compilación rápidos.
¿Me he perdido algo?
Cuando se trabaja en proyectos del "mundo real"
en un proyecto del mundo real, los archivos cpp tienden a incluir encabezados aleatorios hasta que no se puede determinar quién depende de quién
Por supuesto. Pero luego, si tiene tiempo para reorganizar esos archivos para construir su solución "one CPP", entonces tiene tiempo para limpiar esos encabezados. Mis reglas para los encabezados son:
- desglosar el encabezado para que sean lo más modulares posible
- Nunca incluya encabezados que no necesita
- Si necesita un símbolo, reenviar-declararlo
- solo si lo anterior falló, incluya el encabezado
De todos modos, todos los encabezados deben ser autosuficientes, lo que significa:
- Un encabezado incluye todos los encabezados necesarios (y solo los encabezados necesarios, ver arriba)
- un archivo CPP vacío que incluye un encabezado debe compilarse sin necesidad de incluir nada más
Esto eliminará problemas de pedidos y dependencias circulares.
¿Los tiempos de compilación son un problema? Entonces...
Si el tiempo de compilación fuera realmente un problema, consideraría cualquiera de los siguientes:
- Usar encabezados precompilados (esto es bastante útil para STL y BOOST)
- Disminuya el acoplamiento a través del idioma de PImpl, como se explica en http://en.wikipedia.org/wiki/Opaque_pointer
- Usa la compilación compartida de red
Conclusión
Lo que estás haciendo no es poner todo en encabezados.
Básicamente, está incluyendo todos sus archivos en una única fuente final.
Tal vez esté ganando en términos de compilación de proyecto completo.
Pero al compilar por un pequeño cambio, siempre perderás.
Al codificar, sé que compilo a menudo pequeños cambios (aunque solo sea para que el compilador valide mi código), y luego una vez más, haga un cambio completo en el proyecto.
Perdería mucho tiempo si mi proyecto estuviera organizado a su manera.
Una cosa que nadie ha mencionado es que la compilación de archivos grandes requiere mucha memoria. Compilar todo su proyecto de una vez requeriría un espacio de memoria tan grande que simplemente no es factible, incluso si pudiera poner todos los códigos en los encabezados.
Si está utilizando clases de plantilla, tiene que poner toda la implementación en el encabezado de todos modos ...
Compilar todo el proyecto de una vez (a través de un solo archivo base .cpp) debería permitir algo así como "Optimización de todo el programa" o "Optimización de módulos cruzados", que solo está disponible en algunos compiladores avanzados. Esto no es realmente posible con un compilador estándar si está precompilando todos sus archivos .cpp en archivos objeto, y luego vinculando.
La filosofía importante de la Programación Orientada a Objetos radica en tener la ocultación de datos que lleva a las clases encapsuladas con la implementación oculta de los usuarios. Esto es principalmente para proporcionar una capa de abstracción en la que los usuarios de una clase utilizan principalmente las funciones miembro de acceso público, por ejemplo, tipos específicos y estáticos. Entonces, el desarrollador de la clase puede modificar las implementaciones reales siempre que las implementaciones no estén expuestas a los usuarios. Incluso si la implementación es privada y se declara en un archivo de encabezado, cambiar la implementación requerirá que toda la base de código dependiente se vuelva a compilar. Mientras que, si la implementación (definición de las funciones miembro) está en un código fuente (archivo no encabezado), la biblioteca cambia y la base de código dependiente necesita volver a vincularse con la versión revisada de la biblioteca. Si esa biblioteca está vinculada dinámicamente como una biblioteca compartida, mantener la firma de la función (interfaz) igual y la implementación modificada no requiere volver a vincular también. ¿Ventaja? Por supuesto.