web-dev-qa-db-fra.com

Faut-il utiliser des déclarations à terme au lieu d'inclure dans la mesure du possible?

Chaque fois qu'une déclaration de classe utilise une autre classe uniquement comme pointeurs, est-il judicieux d'utiliser une déclaration de classe vers l'avant au lieu d'inclure le fichier d'en-tête afin d'éviter de manière préventive les problèmes de dépendances circulaires? donc, au lieu d'avoir:

//file C.h
#include "A.h"
#include "B.h"

class C{
    A* a;
    B b;
    ...
};

faites ceci à la place:

//file C.h
#include "B.h"

class A;

class C{
    A* a;
    B b;
    ...
};


//file C.cpp
#include "C.h"
#include "A.h"
...

Y a-t-il une raison pour ne pas le faire dans la mesure du possible?

71
Mat

La méthode de déclaration directe est presque toujours meilleure. (Je ne peux pas penser à une situation où l'inclusion d'un fichier où vous pouvez utiliser une déclaration directe est meilleure, mais je ne vais pas dire que c'est toujours mieux au cas où).

Il n'y a pas d'inconvénients aux classes de déclaration directe, mais je peux penser à certains inconvénients pour l'inclusion inutile d'en-têtes:

  • temps de compilation plus long, car toutes les unités de traduction, y compris C.h comprendra également A.h, bien qu'ils n'en aient peut-être pas besoin.

  • en incluant éventuellement d'autres en-têtes dont vous n'avez pas besoin indirectement

  • polluer l'unité de traduction avec des symboles dont vous n'avez pas besoin

  • vous devrez peut-être recompiler les fichiers source qui incluent cet en-tête s'il change (@PeterWood)

56
Luchian Grigore

Oui, il est toujours préférable d'utiliser des déclarations avancées.

Certains des avantages qu'ils offrent sont:

  • Temps de compilation réduit.
  • Aucun espace de noms pollue.
  • (Dans certains cas) peut réduire la taille de vos fichiers binaires générés.
  • Le temps de recompilation peut être considérablement réduit.
  • Éviter le conflit potentiel des noms de préprocesseur.
  • Implémentation Idiome PIMPL fournissant ainsi un moyen de cacher l'implémentation de l'interface.

Cependant, Forward déclarant une classe fait de cette classe particulière un type incomplet et cela limite sévèrement les opérations que vous pouvez effectuer sur le type incomplet.
Vous ne pouvez effectuer aucune opération nécessitant le compilateur pour connaître la disposition de la classe.

Avec le type incomplet, vous pouvez:

  • Déclarez qu'un membre est un pointeur ou une référence au type incomplet.
  • Déclarez des fonctions ou des méthodes qui acceptent/renvoient des types incomplets.
  • Définissez des fonctions ou des méthodes qui acceptent/renvoient des pointeurs/références au type incomplet (mais sans utiliser ses membres).

Avec un type incomplet, vous ne pouvez pas:

  • Utilisez-le comme classe de base.
  • Utilisez-le pour déclarer un membre.
  • Définissez des fonctions ou des méthodes à l'aide de ce type.
36
Alok Save

Y a-t-il une raison pour ne pas le faire dans la mesure du possible?

Commodité.

Si vous savez d'avance que tout utilisateur de ce fichier d'en-tête devra nécessairement inclure également la définition de A pour faire quoi que ce soit (ou peut-être la plupart du temps). Ensuite, il est pratique de l'inclure une fois pour toutes.

C'est un sujet assez délicat, car une utilisation trop libérale de cette règle empirique donnera un code presque incompilable. Notez que Boost aborde le problème différemment en fournissant des en-têtes "de commodité" spécifiques qui regroupent quelques fonctionnalités proches.

18
Matthieu M.

Un cas dans lequel vous ne voulez pas avoir de déclarations à terme, c'est quand elles sont elles-mêmes délicates. Cela peut se produire si certaines de vos classes sont basées sur des modèles, comme dans l'exemple suivant:

// Forward declarations
template <typename A> class Frobnicator;
template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer;

// Alternative: more clear to the reader; more stable code
#include "Gibberer.h"

// Declare a function that does something with a pointer
int do_stuff(Gibberer<int, float>*);

Les déclarations en aval sont les mêmes que la duplication de code: si le code a tendance à beaucoup changer, vous devez le changer à 2 endroits ou plus à chaque fois, et ce n'est pas bon.

11
anatolyg

Faut-il utiliser des déclarations à terme au lieu d'inclure dans la mesure du possible?

Non, les déclarations prévisionnelles explicites ne doivent pas être considérées comme une ligne directrice générale. Les déclarations avancées sont essentiellement copiées et collées, ou du code mal orthographié, qui, si vous y trouvez un bogue, doivent être corrigées partout où les déclarations avancées sont utilisées. Cela peut être sujet aux erreurs.

Pour éviter les incohérences entre les déclarations "avant" et ses définitions, placez les déclarations dans un fichier d'en-tête et incluez ce fichier d'en-tête dans les fichiers source de définition et d'utilisation de déclaration.

Dans ce cas particulier, cependant, où seule une classe opaque est déclarée en avant, cette déclaration en avant peut être acceptable à utiliser, mais en général, "utiliser des déclarations en avant au lieu d'inclure autant que possible", comme le dit le titre de ce fil, peut être assez risqué.

Voici quelques exemples de "risques invisibles" concernant les déclarations à terme (risques invisibles = incompatibilités de déclaration non détectées par le compilateur ou l'éditeur de liens):

  • Les déclarations explicites en aval de symboles représentant des données peuvent être dangereuses, car de telles déclarations en aval peuvent nécessiter une connaissance correcte de l'empreinte (taille) du type de données.

  • Les déclarations directes explicites de symboles représentant des fonctions peuvent également être dangereuses, comme les types de paramètres et le nombre de paramètres.

L'exemple ci-dessous illustre cela, par exemple, deux déclarations vers l'avant dangereuses de données ainsi que d'une fonction:

Fichier a.c:

#include <iostream>
char data[128][1024];
extern "C" void function(short truncated, const char* forgotten) {
  std::cout << "truncated=" << std::hex << truncated
            << ", forgotten=\"" << forgotten << "\"\n";
}

Fichier b.c:

#include <iostream>
extern char data[1280][1024];           // 1st dimension one decade too large
extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param

int main() {
  function(0x1234abcd);                         // In worst case: - No crash!
  std::cout << "accessing data[1270][1023]\n";
  return (int) data[1270][1023];                // In best case:  - Boom !!!!
}

Compiler le programme avec g ++ 4.7.1:

> g++ -Wall -pedantic -ansi a.c b.c

Remarque: danger invisible, car g ++ ne donne aucune erreur/avertissement du compilateur ou de l'éditeur de liens
Remarque: L'omission de extern "C" Entraîne une erreur de liaison pour function() en raison de la modification du nom c ++.

Exécution du programme:

> ./a.out
truncated=abcd, forgotten="♀♥♂☺☻"
accessing data[1270][1023]
Segmentation fault
8
Blue Demon

Fait amusant, dans son guide de style C++ , Google recommande d'utiliser #include Partout mais pour éviter les dépendances circulaires.

8
Zouch

Y a-t-il une raison pour ne pas le faire dans la mesure du possible?

Absolument: il rompt l'encapsulation en exigeant que l'utilisateur d'une classe ou d'une fonction connaisse et duplique les détails de l'implémentation. Si ces détails d'implémentation changent, le code qui déclare vers l'avant peut être rompu tandis que le code qui s'appuie sur l'en-tête continuera de fonctionner.

Déclarer une fonction en avant:

  • nécessite de savoir qu'il est implémenté comme une fonction et non comme une instance d'un objet foncteur statique ou (halètement!) une macro,

  • nécessite la duplication des valeurs par défaut pour les paramètres par défaut,

  • nécessite de connaître son nom et son espace de noms réels, car il peut simplement s'agir d'une déclaration using qui le tire dans un autre espace de noms, peut-être sous un alias, et

  • peut perdre sur l'optimisation en ligne.

Si le code consommateur repose sur l'en-tête, tous ces détails d'implémentation peuvent être modifiés par le fournisseur de fonction sans casser votre code.

Forward déclarant une classe:

  • nécessite de savoir s'il s'agit d'une classe dérivée et de la ou des classes de base dont elle dérive,

  • nécessite de savoir qu'il s'agit d'une classe et pas seulement d'un typedef ou d'une instanciation particulière d'un modèle de classe (ou de savoir qu'il s'agit d'un modèle de classe et d'obtenir tous les paramètres du modèle et les valeurs par défaut corrects),

  • nécessite de connaître le vrai nom et l'espace de noms de la classe, car il peut s'agir d'une déclaration using qui le tire dans un autre espace de noms, peut-être sous un alias, et

  • nécessite de connaître les bons attributs (peut-être qu'il a des exigences d'alignement particulières).

Encore une fois, la déclaration directe rompt l'encapsulation de ces détails d'implémentation, ce qui rend votre code plus fragile.

Si vous avez besoin de réduire les dépendances d'en-tête pour accélérer le temps de compilation, demandez au fournisseur de la classe/fonction/bibliothèque de fournir un en-tête spécial de déclarations directes. La bibliothèque standard le fait avec <iosfwd>. Ce modèle préserve l'encapsulation des détails d'implémentation et donne au mainteneur de bibliothèque la possibilité de modifier ces détails d'implémentation sans casser votre code, tout en réduisant la charge sur le compilateur.

Une autre option consiste à utiliser un idiome pimpl, qui masque encore mieux les détails d'implémentation et accélère les compilations au prix d'un petit temps d'exécution.

5
Adrian McCarthy

Y a-t-il une raison pour ne pas le faire dans la mesure du possible?

La seule raison à laquelle je pense est d'économiser de la frappe.

Sans déclarations avancées, vous ne pouvez inclure le fichier d'en-tête qu'une seule fois, mais je ne conseille pas de le faire sur des projets assez importants en raison des inconvénients signalés par d'autres personnes.

2
ks1322