Lors de l'écriture d'une classe C ++ basée sur des modèles, vous avez généralement trois options:
(1) Mettez la déclaration et la définition dans l'en-tête.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
ou
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Pro:
- Utilisation très pratique (il suffit d'inclure l'en-tête).
Con:
- L'interface et l'implémentation des méthodes sont mixtes. C'est "juste" un problème de lisibilité. Certains trouvent cela impossible à maintenir, car il est différent de l'approche habituelle .h / .cpp. Cependant, sachez que ce n'est pas un problème dans d'autres langages, par exemple, C # et Java.
- Impact de reconstruction élevé: si vous déclarez une nouvelle classe avec
Foo
comme membre, vous devez l'inclure foo.h
. Cela signifie que la modification de l'implémentation de se Foo::f
propage à la fois dans les fichiers d'en-tête et source.
Examinons de plus près l'impact de la reconstruction: pour les classes C ++ non basées sur des modèles, vous placez les déclarations dans .h et les définitions de méthode dans .cpp. De cette façon, lorsque l'implémentation d'une méthode est modifiée, un seul .cpp doit être recompilé. Ceci est différent pour les classes de modèles si le .h contient tout votre code. Jetez un œil à l'exemple suivant:
// 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();
}
Ici, la seule utilisation de Foo::f
est à l'intérieur bar.cpp
. Cependant, si vous modifiez l'implémentation de Foo::f
, les deux bar.cpp
et qux.cpp
doivent être recompilés. L'implémentation de Foo::f
vies dans les deux fichiers, même si aucune partie de Qux
n'utilise directement quoi que ce soit Foo::f
. Pour les grands projets, cela peut bientôt devenir un problème.
(2) Mettez la déclaration en .h et la définition en .tpp et incluez-la 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:
- Utilisation très pratique (il suffit d'inclure l'en-tête).
- Les définitions d'interface et de méthode sont séparées.
Con:
- Impact de reconstruction élevé (identique à (1) ).
Cette solution sépare la déclaration et la définition de la méthode dans deux fichiers distincts, tout comme .h / .cpp. Cependant, cette approche a le même problème de reconstruction que (1) , car l'en-tête inclut directement les définitions de méthode.
(3) Mettez la déclaration dans .h et la définition dans .tpp, mais n'incluez pas .tpp dans .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Pro:
- Réduit l'impact de la reconstruction tout comme la séparation .h / .cpp.
- Les définitions d'interface et de méthode sont séparées.
Con:
- Utilisation peu pratique: lors de l'ajout d'un
Foo
membre à une classe Bar
, vous devez l'inclure foo.h
dans l'en-tête. Si vous appelez Foo::f
un .cpp, vous devez également l' inclure foo.tpp
.
Cette approche réduit l'impact de la reconstruction, car seuls les fichiers .cpp qui utilisent vraiment Foo::f
doivent être recompilés. Cependant, cela a un prix: tous ces fichiers doivent être inclus foo.tpp
. Prenez l'exemple ci-dessus et utilisez la nouvelle approche:
// 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();
}
Comme vous pouvez le voir, la seule différence est l'inclusion supplémentaire de foo.tpp
in bar.cpp
. Cela n'est pas pratique et l'ajout d'un second include pour une classe selon que vous appelez des méthodes semble très moche. Cependant, vous réduisez l'impact de la reconstruction: ne bar.cpp
doit être recompilé que si vous modifiez l'implémentation de Foo::f
. Le fichier qux.cpp
n'a pas besoin de recompilation.
Sommaire:
Si vous implémentez une bibliothèque, vous n'avez généralement pas besoin de vous soucier de l'impact de la reconstruction. Les utilisateurs de votre bibliothèque récupèrent une version et l'utilisent et l'implémentation de la bibliothèque ne change pas dans le travail quotidien de l'utilisateur. Dans de tels cas, la bibliothèque peut utiliser l'approche (1) ou (2) et c'est juste une question de goût que vous choisissez.
Cependant, si vous travaillez sur une application ou si vous travaillez sur une bibliothèque interne de votre entreprise, le code change fréquemment. Vous devez donc vous soucier de l'impact de la reconstruction. Choisir l'approche (3) peut être une bonne option si vous demandez à vos développeurs d'accepter l'inclusion supplémentaire.