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
Foo
como miembro, debe incluirla foo.h
. Esto significa que cambiar la implementación de Foo::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:
// 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::f
está dentro bar.cpp
. Sin embargo, si cambia la implementación de Foo::f
ambos, bar.cpp
y qux.cpp
necesita ser recompilado. La implementación de Foo::f
vidas en ambos archivos, a pesar de que ninguna parte de Qux
utiliza 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
Foo
miembro a una clase Bar
, debe incluirlo foo.h
en el encabezado. Si llama Foo::f
a un .cpp, también debe incluirlo foo.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 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.tpp
in 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.cpp
necesario volver a compilar si cambia la implementación de Foo::f
. El archivo qux.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.