¿Es una buena práctica colocar definiciones de C ++ en los archivos de encabezado?

196

Mi estilo personal con C ++ siempre tiene que poner las declaraciones de clase en un archivo de inclusión y las definiciones en un archivo .cpp, muy parecido a lo estipulado en la respuesta de Loki a los archivos de encabezado de C ++, separación de código . Es cierto que parte de la razón por la que me gusta este estilo probablemente tiene que ver con todos los años que pasé codificando Modula-2 y Ada, los cuales tienen un esquema similar con archivos de especificación y archivos de cuerpo.

Tengo un compañero de trabajo, mucho más experto en C ++ que yo, que insiste en que todas las declaraciones de C ++ deben, cuando sea posible, incluir las definiciones allí mismo en el archivo de encabezado. No está diciendo que este sea un estilo alternativo válido, o incluso un estilo ligeramente mejor, sino que este es el nuevo estilo universalmente aceptado que todos usan ahora para C ++.

Ya no soy tan ágil como solía ser, así que no estoy realmente ansioso por subirme a su carro hasta que vea a unas pocas personas más con él. Entonces, ¿qué tan común es realmente este idioma?

Solo para dar una estructura a las respuestas: ¿es ahora The Way , muy común, algo común, poco común o loco?

TED
fuente
Las funciones de una línea (getters y setters) en el encabezado son comunes. Más tiempo del que obtendría una segunda mirada burlona. ¿Quizás para la definición completa de una clase pequeña que solo otra usa en el mismo encabezado?
Martin Beckett
Siempre he puesto todas mis definiciones de clase en encabezados hasta ahora. solo las definiciones para las clases pimpl son las excepciones. Solo declaro aquellos en encabezados.
Johannes Schaub - litb
3
Tal vez él piensa que es así porque así es como Visual C ++ insiste en que se escriba el código. Cuando hace clic en un botón, la implementación se genera en el archivo de encabezado. Sin embargo, no sé por qué Microsoft alentaría esto por las razones que otros han explicado a continuación.
WKS
44
@WKS: Microsoft prefiere que todos programen en C #, y en C #, no hay distinción "encabezado" frente a "cuerpo", es solo un archivo. Después de haber estado en los mundos C ++ y C # durante mucho tiempo, la forma C # es mucho más fácil de manejar.
Mark Lakata
1
@MarkLakata: esa es una de las cosas que señaló. No he escuchado este argumento de él últimamente, pero IIRC estaba argumentando que Java y C # funcionan de esta manera, y C # era completamente nuevo en ese momento, lo que lo convirtió en una tendencia que todos los idiomas pronto seguirán
TED

Respuestas:

209

Su compañero de trabajo está equivocado, la forma común es y siempre ha sido poner código en archivos .cpp (o cualquier extensión que desee) y declaraciones en los encabezados.

De vez en cuando hay algún mérito para poner código en el encabezado, esto puede permitir una alineación más inteligente por parte del compilador. Pero al mismo tiempo, puede destruir los tiempos de compilación ya que todo el código debe procesarse cada vez que el compilador lo incluye.

Finalmente, a menudo es molesto tener relaciones de objeto circulares (a veces deseadas) cuando todo el código son los encabezados.

En pocas palabras, tenías razón, él está equivocado.

EDITAR: He estado pensando en tu pregunta. Hay un caso donde lo que dice es cierto. plantillas. Muchas bibliotecas "modernas" más nuevas, como boost, hacen un uso intensivo de las plantillas y, a menudo, son "solo encabezado". Sin embargo, esto solo debe hacerse cuando se trata de plantillas, ya que es la única forma de hacerlo cuando se trata de ellas.

EDITAR: algunas personas les gustaría un poco más de aclaración, aquí hay algunas ideas sobre las desventajas de escribir el código "solo encabezado":

Si busca alrededor, verá que muchas personas intentan encontrar una manera de reducir los tiempos de compilación cuando se trata de impulsar. Por ejemplo: Cómo reducir los tiempos de compilación con Boost Asio , que está viendo una compilación de 14s de un solo archivo 1K con boost incluido. Puede que los 14 no parezcan "explotar", pero ciertamente es mucho más largo de lo normal y puede acumularse bastante rápido. Cuando se trata de un gran proyecto. Las bibliotecas de solo encabezado afectan los tiempos de compilación de una manera bastante medible. Simplemente lo toleramos porque el impulso es muy útil.

Además, hay muchas cosas que no se pueden hacer solo en los encabezados (incluso boost tiene bibliotecas a las que debe vincular ciertas partes, como subprocesos, sistema de archivos, etc.). Un ejemplo principal es que no puede tener objetos globales simples en las bibliotecas de encabezado solamente (a menos que recurra a la abominación que es un singleton) ya que se encontrará con múltiples errores de definición. NOTA: Las variables en línea de C ++ 17 harán posible este ejemplo en particular en el futuro.

Como punto final, cuando se usa boost como ejemplo de código de solo encabezado, a menudo se pierde un gran detalle.

Boost es una biblioteca, no un código de nivel de usuario. así que no cambia tan a menudo. En el código de usuario, si coloca todo en los encabezados, cada pequeño cambio hará que tenga que volver a compilar todo el proyecto. Esa es una pérdida de tiempo monumental (y no es el caso de las bibliotecas que no cambian de compilación a compilación). Cuando divide cosas entre encabezado / fuente y mejor aún, use declaraciones directas para reducir las inclusiones, puede ahorrar horas de recompilación cuando se suman a lo largo de un día.

Evan Teran
fuente
16
Estoy bastante seguro de que es de donde lo está obteniendo. Cuando esto surge, trae plantillas. Su argumento es más o menos que debe hacer todo el código de esta manera para mantener la coherencia.
TED
14
ese es un argumento pobre que está haciendo, quédate con tus armas :)
Evan Teran
11
Las definiciones de plantilla pueden estar en archivos CPP si se admite la palabra clave "exportar". Esa es una esquina oscura de C ++ que, por lo que sé, ni siquiera es implementada por la mayoría de las compilaciones.
Andrei Taranchenko
2
Vea la parte inferior de esta respuesta (la parte superior es algo complicada) para ver un ejemplo: stackoverflow.com/questions/555330/…
Evan Teran
3
Comienza a ser significativo para esta discusión en "Hurra, no hay errores de enlace".
Evan Teran
158

El día que los codificadores de C ++ estén de acuerdo en The Way , los corderos se acostarán con los leones, los palestinos abrazarán a los israelíes y los gatos y los perros podrán casarse.

La separación entre los archivos .h y .cpp es principalmente arbitraria en este punto, un vestigio de las optimizaciones del compilador hace mucho tiempo. A mi entender, las declaraciones pertenecen al encabezado y las definiciones pertenecen al archivo de implementación. Pero, eso es solo costumbre, no religión.

Sí, ese Jake.
fuente
140
"El día que los codificadores de C ++ estén de acuerdo en The Way ..." ¡solo quedará un codificador de C ++!
Brian Ensink
9
Pensé que ya estaban de acuerdo en el camino, declaraciones en .h y definiciones en .cpp
hasen
66
Todos somos hombres ciegos y C ++ es un elefante.
Roderick Taylor
¿hábito? Entonces, ¿qué pasa con el uso de .h para definir el alcance? por qué cosa ha sido reemplazado?
Hernán Eche
28

El código en los encabezados generalmente es una mala idea, ya que obliga a la recompilación de todos los archivos que incluyen el encabezado cuando cambia el código real en lugar de las declaraciones. También ralentizará la compilación, ya que deberá analizar el código en cada archivo que incluya el encabezado.

Una razón para tener código en los archivos de encabezado es que generalmente se necesita para que la palabra clave en línea funcione correctamente y cuando se usan plantillas que se están instalando en otros archivos cpp.

Laserallan
fuente
1
"fuerza la recompilación de todos los archivos que incluyen el encabezado cuando cambia el código real en lugar de las declaraciones" Creo que esta es la razón más genuina; También va con el hecho de que las declaraciones en los encabezados cambian con menos frecuencia que la implementación en los archivos .c.
Ninad
20

Lo que podría estar informando a su compañero de trabajo es la noción de que la mayoría de los códigos C ++ deben tener la plantilla para permitir la máxima usabilidad. Y si tiene una plantilla, entonces todo tendrá que estar en un archivo de encabezado, para que el código del cliente pueda verlo y crear una instancia. Si es lo suficientemente bueno para Boost y STL, es lo suficientemente bueno para nosotros.

No estoy de acuerdo con este punto de vista, pero puede ser de donde viene.

JohnMcG
fuente
Creo que tienes razón sobre esto. Cuando lo discutimos, él siempre usa el ejemplo de plantillas, donde más o menos tienes que hacer esto. No estoy de acuerdo con el "tengo que" también, pero mis alternativas son bastante complicadas.
TED
1
@ted: para el código con plantilla, debe colocar la implementación en el encabezado. La palabra clave 'exportar' permite que un compilador admita la separación de la declaración y la definición de plantillas, pero la compatibilidad con la exportación es bastante inexistente. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Michael Burr
Un encabezado, sí, pero no tiene que ser el mismo encabezado. Ver la respuesta de desconocido a continuación.
TED
Eso tiene sentido, pero no puedo decir que me haya encontrado con ese estilo antes.
Michael Burr
14

Creo que tu compañero de trabajo es inteligente y tú también tienes razón.

Lo útil que encontré es que poner todo en los encabezados es que:

  1. No es necesario escribir y sincronizar encabezados y fuentes.

  2. La estructura es simple y ninguna dependencia circular obliga al codificador a hacer una estructura "mejor".

  3. Portátil, fácil de integrar en un nuevo proyecto.

Estoy de acuerdo con el problema del tiempo de compilación, pero creo que deberíamos notar que:

  1. Es muy probable que el cambio del archivo fuente cambie los archivos de encabezado, lo que lleva a que todo el proyecto se vuelva a compilar.

  2. La velocidad de compilación es mucho más rápida que antes. Y si tiene que construir un proyecto con mucho tiempo y alta frecuencia, puede indicar que el diseño de su proyecto tiene fallas. Separar las tareas en diferentes proyectos y módulos puede evitar este problema.

Por último, solo quiero apoyar a tu compañero de trabajo, solo desde mi punto de vista personal.

XU Bin
fuente
3
+1. Nadie, excepto usted, tuvo la idea de que en un proyecto de encabezado solo los tiempos de compilación largos pueden indicar demasiadas dependencias, lo que es un mal diseño. ¡Buen punto! Pero, ¿pueden eliminarse estas dependencias hasta un punto en que el tiempo de compilación sea realmente corto?
TobiMcNamobi
@TobiMcNamobi: Me encanta la idea de "aflojar" para obtener mejores comentarios sobre las malas decisiones de diseño. Sin embargo, en el caso de compilación de solo encabezado vs compilada por separado, si nos conformamos con esa idea, terminamos con una sola unidad de compilación y tiempos de compilación enormes. Incluso cuando el diseño es realmente genial.
Jo So
En otras palabras, la separación entre la interfaz y la implementación es en realidad una parte de su diseño. En C, debe expresar sus decisiones sobre la encapsulación mediante la separación en el encabezado y la implementación.
Jo So
1
Estoy empezando a preguntarme si hay algún inconveniente en absoluto para soltar los encabezados por completo como lo hacen los idiomas modernos.
Jo So
12

A menudo pondré funciones triviales de miembros en el archivo de encabezado, para permitir que se inserten. ¿Pero poner todo el código allí, solo para ser coherente con las plantillas? Eso es una locura.

Recuerde: una consistencia tonta es el duende de las mentes pequeñas .

Mark Ransom
fuente
Sí, yo también hago eso. La regla general que uso parece ser algo así como "si cabe en una línea de código, déjalo en el encabezado".
TED
¿Qué sucede cuando una biblioteca proporciona el cuerpo de una clase de plantilla A<B>en un archivo cpp, y luego el usuario quiere un A<C>?
jww
@jww No lo dije explícitamente, pero las clases de plantilla deberían estar completamente definidas en encabezados para que el compilador pueda instanciarlo con los tipos que necesite. Ese es un requisito técnico, no una elección estilística. Creo que el problema en la pregunta original es que alguien decidió si era bueno para las plantillas, también era bueno para las clases regulares.
Mark Ransom
7

Como dijo Tuomas, tu encabezado debería ser mínimo. Para completar, me expandiré un poco.

Yo personalmente uso 4 tipos de archivos en mis C++proyectos:

  • Público:
  • Encabezado de reenvío: en el caso de plantillas, etc., este archivo obtiene las declaraciones de reenvío que aparecerán en el encabezado.
  • Encabezado: este archivo incluye el encabezado de reenvío, si lo hay, y declara todo lo que deseo que sea público (y define las clases ...)
  • Privado:
  • Encabezado privado: este archivo es un encabezado reservado para la implementación, incluye el encabezado y declara las funciones / estructuras auxiliares (para Pimpl, por ejemplo, o predicados). Omitir si no es necesario.
  • Archivo de origen: incluye el encabezado privado (o encabezado si no hay encabezado privado) y define todo (no plantilla ...)

Además, combino esto con otra regla: no defina lo que puede reenviar. Aunque, por supuesto, soy razonable allí (usar Pimpl en todas partes es bastante complicado).

Significa que prefiero una declaración adelantada sobre una #includedirectiva en mis encabezados siempre que pueda salirse con la suya.

Finalmente, también uso una regla de visibilidad: limito los alcances de mis símbolos tanto como sea posible para que no contaminen los alcances exteriores.

Poniéndolo en conjunto:

// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;

// example.hpp
#include "project/example_fwd.hpp"

// Those can't really be skipped
#include <string>
#include <vector>

#include "project/pimpl.hpp"

// Those can be forward declared easily
#include "project/foo_fwd.hpp"

namespace project { class Bar; }

namespace project
{
  class MyClass
  {
  public:
    struct Color // Limiting scope of enum
    {
      enum type { Red, Orange, Green };
    };
    typedef Color::type Color_t;

  public:
    MyClass(); // because of pimpl, I need to define the constructor

  private:
    struct Impl;
    pimpl<Impl> mImpl; // I won't describe pimpl here :p
  };

  template <class T> class MyClassT: public MyClass {};
} // namespace project

// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"

template <class T> void check(MyClass<T> const& c) { }

// example.cpp
#include "example_impl.hpp"

// MyClass definition

El salvavidas aquí es que la mayoría de las veces el encabezado directo es inútil: solo es necesario en caso de typedefo, templatey también lo es el encabezado de implementación;)

Matthieu M.
fuente
6

Para agregar más diversión, puede agregar .ipparchivos que contienen la implementación de la plantilla (que se incluye en .hpp), mientras que .hppcontiene la interfaz.

Además del código en plantilla (dependiendo del proyecto, puede tratarse de archivos mayoritarios o minoritarios) existe un código normal y aquí es mejor separar las declaraciones y definiciones. Proporcione también declaraciones de reenvío cuando sea necesario; esto puede tener efecto en el tiempo de compilación.

Anónimo
fuente
Eso es lo que hice con las definiciones de plantilla también (aunque no estoy seguro de haber usado la misma extensión ... ha pasado un tiempo).
TED
5

Generalmente, cuando escribo una nueva clase, pondré todo el código en la clase, para que no tenga que buscar en otro archivo. Después de que todo esté funcionando, descompongo el cuerpo de los métodos en el archivo cpp , dejando los prototipos en el archivo hpp.

EvilTeach
fuente
4

Si este nuevo camino es realmente El Camino , podríamos haber estado corriendo en una dirección diferente en nuestros proyectos.

Porque tratamos de evitar todas las cosas innecesarias en los encabezados. Eso incluye evitar la cascada de encabezados. El código en los encabezados probablemente necesitará incluir otro encabezado, que necesitará otro encabezado, etc. Si nos vemos obligados a usar plantillas, intentamos evitar ensuciar demasiado los encabezados con plantillas.

También usamos el patrón "puntero opaco" cuando corresponde.

Con estas prácticas podemos hacer compilaciones más rápidas que la mayoría de nuestros pares. Y sí ... cambiar el código o los miembros de la clase no causará grandes reconstrucciones.

Virne
fuente
4

Yo personalmente hago esto en mis archivos de encabezado:

// class-declaration

// inline-method-declarations

No me gusta mezclar el código de los métodos con la clase, ya que me resulta difícil buscar las cosas rápidamente.

No pondría TODOS los métodos en el archivo de encabezado. El compilador (normalmente) no podrá en línea métodos virtuales y (probablemente) solo en línea pequeños métodos sin bucles (depende totalmente del compilador).

Hacer los métodos en la clase es válido ... pero desde un punto de vista legible no me gusta. Poner los métodos en el encabezado significa que, cuando sea posible, se alinearán.

TofuBeer
fuente
2

En mi humilde opinión, tiene mérito SOLO si está haciendo plantillas y / o metaprogramación. Ya hay muchas razones mencionadas que limitan los archivos de encabezado a solo declaraciones. Son solo eso ... encabezados. Si desea incluir código, lo compila como una biblioteca y lo vincula.

Spoulson
fuente
2

Puse toda la implementación fuera de la definición de clase. Quiero tener los comentarios de doxygen fuera de la definición de clase.

Jesus Fernandez
fuente
1
Sé que es tarde, pero los downvoters (o simpatizantes) quieren comentar por qué. Esto me parece una declaración razonable. Usamos Doxygen, y el problema ciertamente surgió.
TED
2

Creo que es absolutamente absurdo poner TODAS las definiciones de funciones en el archivo de encabezado. ¿Por qué? Porque el archivo de encabezado se usa como la interfaz PÚBLICA para su clase. Es el exterior de la "caja negra".

Cuando necesite mirar una clase para hacer referencia a cómo usarla, debería mirar el archivo de encabezado. El archivo de encabezado debe dar una lista de lo que puede hacer (comentado para describir los detalles de cómo usar cada función), y debe incluir una lista de las variables miembro. NO DEBE incluir CÓMO se implementa cada función individual, porque esa es una carga de información innecesaria y solo llena el archivo de encabezado.

SeanRamey
fuente
1

¿Eso realmente no depende de la complejidad del sistema y de las convenciones internas?

En este momento estoy trabajando en un simulador de red neuronal que es increíblemente complejo, y el estilo aceptado que se espera que use es:

Definiciones de clase en classname.h
Código de clase en classnameCode.h
código ejecutable en classname.cpp

Esto divide las simulaciones creadas por el usuario a partir de las clases base creadas por el desarrollador, y funciona mejor en la situación.

Sin embargo, me sorprendería ver que la gente hace esto en, por ejemplo, una aplicación de gráficos o cualquier otra aplicación cuyo propósito no sea proporcionar a los usuarios una base de código.

Ed James
fuente
1
¿Cuál es exactamente la distinción entre "Código de clase" y "Código ejecutable"?
TED
Como dije, es un simulador neuronal: el usuario crea simulaciones ejecutables que se basan en una gran cantidad de clases que actúan como neuronas, etc. Por lo tanto, nuestro código es simplemente clases que no pueden hacer nada por sí mismas, y el usuario crea el código ejecutable eso hace que el simulador haga cosas.
Ed James
En general, ¿no podría decir "no puede hacer nada por sí mismo" para la gran mayoría (si no la totalidad) de la mayoría de los programas? ¿Estás diciendo que el código "principal" va en un cpp, pero nada más?
TED
En esta situación es un poco diferente. El código que escribimos es básicamente una biblioteca, y el usuario construye sus simulaciones además de esto, que en realidad son ejecutables. Piénsalo como openGL -> obtienes un montón de funciones y objetos, pero sin un archivo cpp que pueda ejecutarlos son inútiles.
Ed James
0

El código de la plantilla debe estar solo en los encabezados. Aparte de eso, todas las definiciones, excepto las en línea, deben estar en .cpp. El mejor argumento para esto sería las implementaciones de la biblioteca estándar que siguen la misma regla. No estaría en desacuerdo con que los desarrolladores de std lib estarían en lo correcto al respecto.

Abhishek Agarwal
fuente
¿Qué stdlibs? libstdc++Parece que el GCC (AFAICS) no pone casi nada srcy casi todo dentro include, ya sea que 'deba' estar o no en un encabezado. Así que no creo que esta sea una cita precisa / útil. De todos modos, no creo que stdlibs sea un gran modelo para el código de usuario: obviamente están escritos por codificadores altamente calificados, pero para ser utilizados , no leídos: abstraen la alta complejidad en la que la mayoría de los codificadores no deberían pensar , necesita feo en _Reserved __namestodas partes para evitar conflictos con el usuario, los comentarios y el espaciado están por debajo de lo que aconsejaría, etc. Son ejemplares de una manera estrecha.
underscore_d
0

Creo que su compañero de trabajo tiene razón siempre que no entre en el proceso de escribir código ejecutable en el encabezado. El equilibrio correcto, creo, es seguir la ruta indicada por GNAT Ada donde el archivo .ads ofrece una definición de interfaz perfectamente adecuada del paquete para sus usuarios y para sus hijos.

Por cierto, Ted, ¿ha echado un vistazo en este foro a la pregunta reciente sobre el enlace de Ada a la biblioteca CLIPS que escribió hace varios años y que ya no está disponible (las páginas web relevantes ahora están cerradas). Incluso si se hace a una versión antigua de Clips, este enlace podría ser un buen ejemplo de inicio para alguien dispuesto a usar el motor de inferencia CLIPS dentro de un programa Ada 2012.

Emile
fuente
1
Jajaja 2 años después, esta es una forma extraña de contactar a alguien. Comprobaré si todavía tengo una copia, pero lo más probable es que no. Hice eso para una clase de inteligencia artificial para poder hacer mi código en Ada, pero a propósito hice ese proyecto CC0 (esencialmente sin derechos de autor) con la esperanza de que alguien lo tomara descaradamente y hiciera algo con él.
TED