C ++ Méthode préférée de traitement de l'implémentation pour les grands modèles

10

En règle générale, lors de la déclaration d'une classe C ++, il est recommandé de ne placer que la déclaration dans le fichier d'en-tête et de placer l'implémentation dans un fichier source. Cependant, il semble que ce modèle de conception ne fonctionne pas pour les classes de modèles.

Lorsque vous regardez en ligne, il semble y avoir 2 opinions sur la meilleure façon de gérer les classes de modèles:

1. Déclaration complète et mise en œuvre dans l'en-tête.

Ceci est assez simple mais conduit à ce qui, à mon avis, est difficile à maintenir et à modifier des fichiers de code lorsque le modèle devient volumineux.

2. Écrivez l'implémentation dans un fichier d'inclusion de modèle (.tpp) inclus à la fin.

Cela me semble être une meilleure solution mais ne semble pas être largement appliqué. Y a-t-il une raison pour laquelle cette approche est inférieure?

Je sais que le style de code est souvent dicté par des préférences personnelles ou un style hérité. Je démarre un nouveau projet (portage d'un ancien projet C vers C ++) et je suis relativement nouveau dans la conception OO et je souhaite suivre les meilleures pratiques dès le départ.

fhorrobine
la source
1
Voir cet article de 9 ans sur codeproject.com. La méthode 3 correspond à ce que vous avez décrit. Cela ne semble pas être si spécial que vous le pensez.
Doc Brown
.. ou ici, même approche, article de 2014: codeofhonour.blogspot.com/2014/11/…
Doc Brown
2
Étroitement liés: stackoverflow.com/q/1208028/179910 . Gnu utilise généralement une extension ".tcc" au lieu de ".tpp", mais elle est à peu près identique.
Jerry Coffin
J'ai toujours utilisé "ipp" comme extension, mais j'ai fait la même chose beaucoup dans le code que j'ai écrit.
Sebastian Redl

Réponses:

6

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 Foocomme membre, vous devez l'inclure foo.h. Cela signifie que la modification de l'implémentation de se Foo::fpropage à 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::fest à l'intérieur bar.cpp. Cependant, si vous modifiez l'implémentation de Foo::f, les deux bar.cppet qux.cppdoivent être recompilés. L'implémentation de Foo::fvies dans les deux fichiers, même si aucune partie de Quxn'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 Foomembre à une classe Bar, vous devez l'inclure foo.hdans l'en-tête. Si vous appelez Foo::fun .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::fdoivent ê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.tppin 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.cppdoit être recompilé que si vous modifiez l'implémentation de Foo::f. Le fichier qux.cppn'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.

pschill
la source
2

Semblable à l' .tppidée (que je n'ai jamais vue utilisée), nous mettons la plupart des fonctionnalités en ligne dans un -inl.hppfichier qui est inclus à la fin du .hppfichier habituel .

Comme d'autres l'indiquent, cela maintient l'interface lisible en déplaçant l'encombrement des implémentations en ligne (comme les modèles) dans un autre fichier. Nous autorisons certaines interfaces en ligne, mais essayons de les limiter à de petites fonctions, généralement à une seule ligne.

Bill Door
la source
1

Une pièce pro de la 2ème variante est que vos en-têtes sont plus rangés.

Le problème peut être que la vérification des erreurs IDE en ligne et les liaisons du débogueur peuvent être vissées.

πάντα ῥεῖ
la source
2nd nécessite également beaucoup de redondance de déclaration de paramètres de modèle, qui peut devenir très verbeux, surtout lors de l'utilisation de sfinae. Et contrairement à l'OP, je trouve que le 2e est plus difficile à lire, plus il y a de code, en particulier à cause du passe-partout redondant.
Sopel
0

Je préfère grandement l'approche consistant à mettre l'implémentation dans un fichier séparé et à avoir uniquement la documentation et les déclarations dans le fichier d'en-tête.

Peut-être que la raison pour laquelle vous n'avez pas beaucoup utilisé cette approche dans la pratique est que vous n'avez pas cherché aux bons endroits ;-)

Ou - c'est peut-être parce qu'il faut un peu d'effort supplémentaire pour développer le logiciel. Mais pour une bibliothèque de classe, cet effort en vaut bien la peine, à mon humble avis, et se paie dans une bibliothèque beaucoup plus facile à utiliser / lire.

Prenez cette bibliothèque par exemple: https://github.com/SophistSolutions/Stroika/

La bibliothèque entière est écrite avec cette approche et si vous regardez à travers le code, vous verrez à quel point cela fonctionne.

Les fichiers d'en-tête sont à peu près aussi longs que les fichiers d'implémentation, mais ils ne sont remplis que de déclarations et de documentation.

Comparez la lisibilité de Stroika avec celle de votre implémentation std c ++ préférée (gcc ou libc ++ ou msvc). Ceux-ci utilisent tous l'approche d'implémentation en ligne en-tête, et bien qu'ils soient extrêmement bien écrits, à mon humble avis, pas en tant qu'implémentations lisibles.

Lewis Pringle
la source