C ++ Método preferido para tratar la implementación de plantillas grandes

10

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.

forrobin
fuente
1
Vea este artículo de 9 años en codeproject.com. El método 3 es lo que describiste. No parece ser tan especial como crees.
Doc Brown
.. o aquí, mismo enfoque, artículo de 2014: codeofhonour.blogspot.com/2014/11/…
Doc Brown
2
Muy relacionado: stackoverflow.com/q/1208028/179910 . Gnu generalmente usa una extensión ".tcc" en lugar de ".tpp", pero por lo demás es bastante idéntico.
Jerry Coffin
Siempre usé "ipp" como extensión, pero hice lo mismo en el código que escribí.
Sebastian Redl

Respuestas:

6

Al escribir una clase de C ++ con plantilla, generalmente tiene tres opciones:

(1) Ponga declaración y definición en el encabezado.

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f()
    {
        ...
    }
};

o

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f();
};

template <typename T>
inline void Foo::f()
{
    ...
}

Pro:

  • Uso muy conveniente (solo incluye el encabezado).

Estafa:

  • La implementación de la interfaz y el método son mixtos. Esto es "solo" un problema de legibilidad. Algunos encuentran que esto no se puede mantener, porque es diferente del enfoque habitual .h / .cpp. Sin embargo, tenga en cuenta que esto no es un problema en otros lenguajes, por ejemplo, C # y Java.
  • Alto impacto de reconstrucción: si declara una nueva clase Foocomo miembro, debe incluirla foo.h. Esto significa que cambiar la implementación de Foo::fpropaga 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:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Aquí, el único uso de Foo::festá dentro bar.cpp. Sin embargo, si cambia la implementación de Foo::fambos, bar.cppy qux.cppnecesita ser recompilado. La implementación de Foo::fvidas en ambos archivos, a pesar de que ninguna parte de Quxutiliza directamente nada de Foo::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.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};
#include "foo.tpp"    

// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
    ...
}

Pro:

  • Uso muy conveniente (solo incluye el encabezado).
  • Las definiciones de interfaz y método están separadas.

Estafa:

  • Alto impacto de reconstrucción (igual que (1) ).

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.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};

// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
    ...
}

Pro:

  • Reduce el impacto de reconstrucción al igual que la separación .h / .cpp.
  • Las definiciones de interfaz y método están separadas.

Estafa:

  • Uso inconveniente: al agregar un Foomiembro a una clase Bar, debe incluirlo foo.hen el encabezado. Si llama Foo::fa un .cpp, también debe incluirlo foo.tppallí.

Este enfoque reduce el impacto de la reconstrucción, ya que solo los archivos .cpp que realmente usan Foo::fnecesitan ser recompilados. Sin embargo, esto tiene un precio: todos esos archivos deben incluir foo.tpp. Tome el ejemplo de arriba y use el nuevo enfoque:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Como puede ver, la única diferencia es la inclusión adicional de foo.tppin bar.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 es bar.cppnecesario volver a compilar si cambia la implementación de Foo::f. El archivo qux.cppno 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.

pschill
fuente
2

Similar a la .tppidea (que nunca he visto utilizada), ponemos la mayor parte de la funcionalidad en línea en un -inl.hpparchivo que se incluye al final del .hpparchivo 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.

Bill Door
fuente
1

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
2nd también requiere una gran cantidad de redundancia de declaración de parámetros de plantilla, que puede volverse muy detallada, especialmente cuando se usa sfinae. Y al contrario del OP, encuentro que el segundo es más difícil de leer cuanto más código haya, específicamente debido a la repetitiva placa repetitiva.
Sopel
0

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.

Lewis Pringle
fuente