Por lo general, cuando se declara una clase C ++, se recomienda colocar solo la declaración en el archivo de encabezado y poner la implementación en un archivo fuente. Sin embargo, parece que este modelo de diseño no funciona para las clases de plantilla.
Al mirar en línea, parece haber 2 opiniones sobre la mejor manera de administrar las clases de plantilla:
1. Declaración completa e implementación en encabezado.
Esto es bastante sencillo, pero conduce a lo que, en mi opinión, es difícil de mantener y editar archivos de código cuando la plantilla se vuelve grande.
2. Escriba la implementación en un archivo de inclusión de plantilla (.tpp) incluido al final.
Esto parece una mejor solución para mí, pero no parece aplicarse ampliamente. ¿Hay alguna razón por la cual este enfoque es inferior?
Sé que muchas veces el estilo del código está dictado por la preferencia personal o el estilo heredado. Estoy comenzando un nuevo proyecto (portando un antiguo proyecto C a C ++) y soy relativamente nuevo en el diseño OO y me gustaría seguir las mejores prácticas desde el principio.
fuente
Respuestas:
Al escribir una clase de C ++ con plantilla, generalmente tiene tres opciones:
(1) Ponga declaración y definición en el encabezado.
o
Pro:
Estafa:
Foo
como miembro, debe incluirlafoo.h
. Esto significa que cambiar la implementación deFoo::f
propaga a través de los archivos de cabecera y fuente.Echemos un vistazo más de cerca al impacto de la reconstrucción: para las clases de C ++ sin plantillas, puede colocar declaraciones en .h y definiciones de métodos en .cpp. De esta manera, cuando se cambia la implementación de un método, solo se necesita volver a compilar un .cpp. Esto es diferente para las clases de plantilla si el .h contiene todo su código. Eche un vistazo al siguiente ejemplo:
Aquí, el único uso de
Foo::f
está dentrobar.cpp
. Sin embargo, si cambia la implementación deFoo::f
ambos,bar.cpp
yqux.cpp
necesita ser recompilado. La implementación deFoo::f
vidas en ambos archivos, a pesar de que ninguna parte deQux
utiliza directamente nada deFoo::f
. Para proyectos grandes, esto pronto puede convertirse en un problema.(2) Ponga la declaración en .h y la definición en .tpp e inclúyala en .h.
Pro:
Estafa:
Esta solución separa la declaración y la definición del método en dos archivos separados, como .h / .cpp. Sin embargo, este enfoque tiene el mismo problema de reconstrucción que (1) , porque el encabezado incluye directamente las definiciones de método.
(3) Ponga la declaración en .h y la definición en .tpp, pero no incluya .tpp en .h.
Pro:
Estafa:
Foo
miembro a una claseBar
, debe incluirlofoo.h
en el encabezado. Si llamaFoo::f
a un .cpp, también debe incluirlofoo.tpp
allí.Este enfoque reduce el impacto de la reconstrucción, ya que solo los archivos .cpp que realmente usan
Foo::f
necesitan ser recompilados. Sin embargo, esto tiene un precio: todos esos archivos deben incluirfoo.tpp
. Tome el ejemplo de arriba y use el nuevo enfoque:Como puede ver, la única diferencia es la inclusión adicional de
foo.tpp
inbar.cpp
. Esto es inconveniente y agregar una segunda inclusión para una clase, dependiendo de si llama a los métodos, parece muy feo. Sin embargo, reduce el impacto de la reconstrucción: solo esbar.cpp
necesario volver a compilar si cambia la implementación deFoo::f
. El archivoqux.cpp
no necesita recompilación.Resumen:
Si implementa una biblioteca, generalmente no necesita preocuparse por el impacto de la reconstrucción. Los usuarios de su biblioteca toman una versión y la usan, y la implementación de la biblioteca no cambia en el trabajo diario del usuario. En tales casos, la biblioteca puede usar el enfoque (1) o (2) y es solo cuestión de gustos cuál elegir.
Sin embargo, si está trabajando en una aplicación, o si está trabajando en una biblioteca interna de su empresa, el código cambia con frecuencia. Por lo tanto, debe preocuparse por el impacto de la reconstrucción. Elegir el enfoque (3) puede ser una buena opción si logra que sus desarrolladores acepten la inclusión adicional.
fuente
Similar a la
.tpp
idea (que nunca he visto utilizada), ponemos la mayor parte de la funcionalidad en línea en un-inl.hpp
archivo que se incluye al final del.hpp
archivo habitual .Como indican los demás, esto mantiene la interfaz legible al mover el desorden de las implementaciones en línea (como plantillas) en otro archivo. Permitimos algunas líneas en la interfaz, pero tratamos de limitarlas a funciones pequeñas, generalmente de una sola línea.
fuente
Una moneda profesional de la segunda variante es que tus encabezados se ven más ordenados.
La desventaja es que puede tener la comprobación de errores IDE en línea y los enlaces del depurador estropeados.
fuente
Prefiero en gran medida el enfoque de poner la implementación en un archivo separado y tener solo la documentación y las declaraciones en el archivo de encabezado.
Quizás la razón por la que no ha visto este enfoque utilizado en la práctica es que no ha buscado en los lugares correctos ;-)
O bien, tal vez es porque requiere un poco de esfuerzo adicional para desarrollar el software. Pero para una biblioteca de clase, ese esfuerzo vale la pena, en mi humilde opinión, y se amortiza en una biblioteca mucho más fácil de usar / leer.
Tome esta biblioteca por ejemplo: https://github.com/SophistSolutions/Stroika/
La biblioteca completa está escrita con este enfoque y si observa el código, verá cuán bien funciona.
Los archivos de encabezado son casi tan largos como los archivos de implementación, pero están llenos de nada más que declaraciones y documentación.
Compare la legibilidad de Stroika con la de su implementación std c ++ favorita (gcc o libc ++ o msvc). Todos ellos usan el enfoque de implementación en línea en el encabezado, y aunque están extremadamente bien escritos, en mi humilde opinión, no son implementaciones legibles.
fuente