web-dev-qa-db-fra.com

Séparer le code de classe C ++ en plusieurs fichiers, quelles sont les règles?

Temps de réflexion - Pourquoi voulez-vous quand même fractionner votre fichier?

Comme le titre le suggère, le problème final que j'ai est les erreurs de l'éditeur de définitions multiples. J'ai en fait résolu le problème, mais je ne l'ai pas résolu correctement. Avant de commencer, je veux discuter des raisons de diviser un fichier de classe en plusieurs fichiers. J'ai essayé de mettre tous les scénarios possibles ici - si j'en ai manqué, rappelez-le-moi et je pourrai apporter des modifications. J'espère que les éléments suivants sont corrects:

Raison 1 Pour économiser de l'espace:

Vous disposez d'un fichier contenant la déclaration d'une classe avec tous les membres de la classe. Vous placez #include gardes autour de ce fichier (ou #pragma une fois) pour vous assurer qu'aucun conflit ne survient si vous #incluez le fichier dans deux fichiers d'en-tête différents qui sont ensuite inclus dans un fichier source. Vous compilez un fichier source séparé avec l'implémentation de toutes les méthodes déclarées dans cette classe, car il charge de nombreuses lignes de code à partir de votre fichier source, ce qui nettoie un peu les choses et introduit un certain ordre dans votre programme.

Exemple: Comme vous pouvez le voir, l'exemple ci-dessous pourrait être amélioré en divisant l'implémentation des méthodes de classe dans un fichier différent. (Un fichier .cpp)

// my_class.hpp
#pragma once

class my_class
{
public:
    void my_function()
    {
        // LOTS OF CODE
        // CONFUSING TO DEBUG
        // LOTS OF CODE
        // DISORGANIZED AND DISTRACTING
        // LOTS OF CODE
        // LOOKS HORRIBLE
        // LOTS OF CODE
        // VERY MESSY
        // LOTS OF CODE
    }

    // MANY OTHER METHODS
    // MEANS VERY LARGE FILE WITH LOTS OF LINES OF CODE
}

Raison 2 Pour éviter les erreurs de l'éditeur de définitions multiples:

C'est peut-être la principale raison pour laquelle vous sépareriez la mise en œuvre de la déclaration. Dans l'exemple ci-dessus, vous pouvez déplacer le corps de la méthode en dehors de la classe. Cela le rendrait beaucoup plus propre et structuré. Cependant, selon ceci question , l'exemple ci-dessus a des spécificateurs inline implicites. Le déplacement de l'implémentation de l'intérieur de la classe vers l'extérieur de la classe, comme dans l'exemple ci-dessous, vous causera des erreurs de l'éditeur de liens et vous devrez donc soit tout aligner, soit déplacer les définitions de fonction dans un fichier .cpp.

Exemple: _L'exemple ci-dessous provoquera des "erreurs de l'éditeur de définitions multiples" si vous ne déplacez pas la définition de fonction dans un fichier .cpp ou ne spécifiez pas la fonction en ligne.

// my_class.hpp
void my_class::my_function()
{
    // ERROR! MULTIPLE DEFINITION OF my_class::my_function
    // This error only occurs if you #include the file containing this code
    // in two or more separate source (compiled, .cpp) files.
}

Pour résoudre le problème:

//my_class.cpp
void my_class::my_function()
{
    // Now in a .cpp file, so no multiple definition error
}

Ou:

// my_class.hpp
inline void my_class::my_function()
{
    // Specified function as inline, so okay - note: back in header file!
    // The very first example has an implicit `inline` specifier
}

Reason 3 Vous voulez économiser de l'espace, encore une fois, mais cette fois, vous travaillez avec un modèle classe:

Si nous travaillons avec des classes de modèles, nous ne pouvons pas déplacer l'implémentation vers un fichier source (fichier .cpp). Ce n'est actuellement pas autorisé par (je suppose) ni la norme ni les compilateurs actuels. Contrairement au premier exemple de Reason 2, ci-dessus, nous sommes autorisés à placer l'implémentation dans le fichier d'en-tête. Selon cela question la raison en est que les méthodes de classe de modèle ont également impliqué des spécificateurs inline. Est-ce exact? (Cela semble logique.) Mais personne ne semblait connaître la question que je viens de mentionner!

Alors, les deux exemples ci-dessous sont-ils identiques?

// some_header_file.hpp
#pragma once

// template class declaration goes here
class some_class
{
    // Some code
};

// Example 1: NO INLINE SPECIFIER
template<typename T>
void some_class::class_method()
{
    // Some code
}

// Example 2: INLINE specifier used
template<typename T>
inline void some_class::class_method()
{
    // Some code
}

Si vous avez un fichier d'en-tête de classe de modèle, qui devient énorme en raison de toutes les fonctions que vous avez, je pense que vous êtes autorisé à déplacer les définitions de fonction vers un autre fichier d'en-tête (généralement un fichier .tpp?), Puis #include file.tpp à la fin de votre fichier d'en-tête contenant la déclaration de classe. Cependant, vous ne devez PAS inclure ce fichier ailleurs, d'où le .tpp Plutôt que .hpp.

Je suppose que vous pouvez également le faire avec les méthodes en ligne d'une classe régulière? Est-ce également permis?

Heure des questions

J'ai donc fait quelques déclarations ci-dessus, dont la plupart concernent la structuration des fichiers source. Je pense que tout ce que j'ai dit était correct, parce que j'ai fait des recherches fondamentales et "découvert des trucs", mais c'est une question et donc je ne sais pas avec certitude.

Cela se résume à la façon dont vous organiseriez le code dans les fichiers. Je pense avoir trouvé une structure qui fonctionnera toujours.

Voici ce que j'ai trouvé. (C'est "Norme d'organisation/structure de fichier de code de classe d'Ed Bird", si vous le souhaitez. Je ne sais pas encore si ce sera très utile, c'est le point de demander.)

  • 1: Déclarez la classe (modèle ou autre) dans un fichier .hpp, Y compris toutes les méthodes, fonctions ami et données.
  • 2: Au bas du fichier .hpp, #include Un fichier .tpp Contenant l'implémentation de toutes les méthodes inline. Créez le fichier .tpp Et assurez-vous que toutes les méthodes sont spécifiées comme inline.
  • 3: Tous les autres membres (fonctions non en ligne, fonctions ami et données statiques) doivent être définis dans un fichier .cpp, Qui #include S le fichier .hpp En haut pour éviter les erreurs comme "la classe ABC n'a pas été déclarée". Étant donné que tout dans ce fichier aura un lien externe, le programme sera correctement lié.

Des normes comme celle-ci existent-elles dans l'industrie? Est-ce que la norme que j'ai trouvée fonctionne dans tous les cas?

25
user3728501

Vos trois points sonnent juste. C'est la façon standard de faire les choses (bien que je n'aie jamais vu l'extension .tpp auparavant, c'est généralement .inl), bien que personnellement, je mette simplement les fonctions en ligne au bas des fichiers d'en-tête plutôt que dans un fichier séparé.

Voici comment j'organise mes fichiers. J'omet le fichier forward declare pour les classes simples.

myclass-fwd.h

#pragma once

namespace NS
{
class MyClass;
}

myclass.h

#pragma once
#include "headers-needed-by-header"
#include "myclass-fwd.h"

namespace NS
{
class MyClass
{
    ..
};
}

myclass.cpp

#include "headers-needed-by-source"
#include "myclass.h"

namespace
    {
    void LocalFunc();
}

NS::MyClass::...

Remplacez le pragma par des protège-têtes selon vos préférences.

La raison de cette approche est de réduire les dépendances d'en-tête, ce qui ralentit les temps de compilation dans les grands projets. Si vous ne le saviez pas, vous pouvez transmettre une classe à utiliser comme pointeur ou référence. La déclaration complète n'est nécessaire que lorsque vous construisez, créez ou utilisez des membres de la classe.

Cela signifie qu'une autre classe qui utilise la classe (prend les paramètres par pointeur/référence) n'a qu'à inclure l'en-tête fwd dans son propre en-tête. L'en-tête complet est ensuite inclus dans le fichier source de la deuxième classe. Cela réduit considérablement la quantité de déchets inutiles que vous obtenez en tirant dans un gros en-tête, qui en tire un autre en-tête, qui en tire un autre ...

L'astuce suivante est l'espace de noms sans nom (parfois appelé espace de noms anonyme). Cela ne peut apparaître que dans un fichier source et c'est comme un espace de noms caché uniquement visible par ce fichier. Vous pouvez placer ici des fonctions locales, des classes, etc. qui ne sont utilisées que par le fichier source. Cela empêche les conflits de noms si vous créez quelque chose avec le même nom dans deux fichiers différents. (Deux fonctions locales F par exemple, peuvent donner des erreurs de l'éditeur de liens).

4
Neil Kirk

La principale raison de séparer l'interface de l'implémentation est de ne pas avoir à recompiler tout votre code lorsqu'un élément de l'implémentation change; il vous suffit de recompiler les fichiers source modifiés.

Quant à "Déclarer la classe (modèle ou autre)", un template est pas un class. Un template est un modèle pour les classes création. Plus important, cependant, vous définir une classe ou un modèle dans un en-tête. La définition de classe inclut des déclarations de ses fonctions membres et les fonctions membres non inines sont définies dans un ou plusieurs fichiers source. Les fonctions membres en ligne et toutes les fonctions de modèle doivent être définies dans l'en-tête, par n'importe quelle combinaison de définitions directes et #include directives que vous préférez.

2
Pete Becker

Des normes comme celle-ci existent-elles dans l'industrie?

Oui. Là encore, des normes de codage assez différentes de celles que vous avez exprimées se retrouvent également dans l'industrie. Vous parlez de normes de codage, après tout, et les normes de codage vont du bon au mauvais au laid.

Est-ce que la norme que j'ai trouvée fonctionne dans tous les cas?

Absolument pas. Par exemple,

template <typename T> class Foo {
public:
   void some_method (T& arg);
...
};

Ici, la définition du modèle de classe Foo ne sait rien de ce paramètre de modèle T. Et si, pour certains modèles de classe, les définitions des méthodes varient en fonction des paramètres du modèle? Votre règle n ° 2 ne fonctionne tout simplement pas ici.

Un autre exemple: que se passe-t-il si le fichier source correspondant est énorme, long de mille lignes ou plus? Parfois, il est logique de fournir l'implémentation dans plusieurs fichiers source. Certaines normes vont à l'extrême de dicter une fonction par fichier (avis personnel: Yech!).

À l'autre extrême d'un fichier source long de plus de mille lignes se trouve une classe qui n'a pas de fichiers source. L'implémentation entière est dans l'en-tête. Il y a beaucoup à dire pour les implémentations uniquement en-tête. Si rien d'autre, cela simplifie, parfois de manière significative, le problème de liaison.

0
David Hammen