Je cherche la définition du moment où je suis autorisé à faire la déclaration en aval d'une classe dans le fichier d'en-tête d'une autre classe:
Suis-je autorisé à le faire pour une classe de base, pour une classe détenue en tant que membre, pour une classe transmise à fonction membre par référence, etc.?
Mettez-vous à la place du compilateur: lorsque vous déclarez un type, tout ce que le compilateur sait est que ce type existe; il ne sait rien de sa taille, de ses membres ou de ses méthodes. C'est pourquoi il est appelé un type incomplet . Par conséquent, vous ne pouvez pas utiliser le type pour déclarer un membre ou une classe de base, car le compilateur doit connaître la présentation du type.
En supposant la déclaration suivante suivante.
class X;
Voici ce que vous pouvez et ne pouvez pas faire.
Ce que vous pouvez faire avec un type incomplet:
Déclarez un membre comme étant un pointeur ou une référence au type incomplet:
class Foo {
X *p;
X &r;
};
Déclare des fonctions ou méthodes qui acceptent/retournent des types incomplets:
void f1(X);
X f2();
Définissez les fonctions ou méthodes qui acceptent/renvoient les pointeurs/références au type incomplet (mais sans utiliser ses membres):
void f3(X*, X&) {}
X& f4() {}
X* f5() {}
Ce que vous ne pouvez pas faire avec un type incomplet:
Utilisez-le comme classe de base
class Foo : X {} // compiler error!
Utilisez-le pour déclarer un membre:
class Foo {
X m; // compiler error!
};
Définir des fonctions ou des méthodes utilisant ce type
void f1(X x) {} // compiler error!
X f2() {} // compiler error!
Utiliser ses méthodes ou ses champs, en essayant en fait de déréférencer une variable de type incomplet
class Foo {
X *m;
void method()
{
m->someMethod(); // compiler error!
int i = m->someField; // compiler error!
}
};
En ce qui concerne les modèles, il n'y a pas de règle absolue: la possibilité d'utiliser un type incomplet en tant que paramètre de modèle dépend de la manière dont le type est utilisé dans le modèle.
Par exemple, std::vector<T>
requiert que son paramètre soit de type complet, contrairement à boost::container::vector<T>
. Parfois, un type complet n'est requis que si vous utilisez certaines fonctions membres. c'est le cas pour std::unique_ptr<T>
, par exemple.
Un modèle bien documenté devrait indiquer dans sa documentation toutes les exigences relatives à ses paramètres, y compris s’il s’agit de types complets ou non.
La règle principale est que vous ne pouvez déclarer que les classes dont la structure de mémoire (et donc les fonctions membres et les membres de données) n'a pas besoin d'être connue dans le fichier que vous transmettez par la déclaration.
Cela exclurait les classes de base et tout sauf les classes utilisées via les références et les pointeurs.
Lakos distingue l'utilisation de la classe
Je ne l'ai jamais vu se prononcer plus succinctement :)
En plus des pointeurs et des références aux types incomplets, vous pouvez également déclarer des prototypes de fonctions spécifiant des paramètres et/ou renvoyant des valeurs qui sont des types incomplets. Cependant, vous ne pouvez pas définir une fonction ayant un paramètre ou un type de retour incomplet, à moins que ce ne soit un pointeur ou une référence.
Exemples:
struct X; // Forward declaration of X
void f1(X* px) {} // Legal: can always use a pointer
void f2(X& x) {} // Legal: can always use a reference
X f3(int); // Legal: return value in function prototype
void f4(X); // Legal: parameter in function prototype
void f5(X) {} // ILLEGAL: *definitions* require complete types
Aucune des réponses à ce jour ne décrit quand on peut utiliser une déclaration forward d'un modèle de classe. Alors, voilà.
Un modèle de classe peut être transféré et déclaré comme:
template <typename> struct X;
En suivant la structure du réponse acceptée ,
Voici ce que vous pouvez et ne pouvez pas faire.
Ce que vous pouvez faire avec un type incomplet:
Déclarez un membre comme étant un pointeur ou une référence au type incomplet dans un autre modèle de classe:
template <typename T>
class Foo {
X<T>* ptr;
X<T>& ref;
};
Déclarez un membre comme étant un pointeur ou une référence à l'une de ses instanciations incomplètes:
class Foo {
X<int>* ptr;
X<int>& ref;
};
Déclarez les modèles de fonction ou les modèles de fonction de membre qui acceptent/renvoient les types incomplets:
template <typename T>
void f1(X<T>);
template <typename T>
X<T> f2();
Déclarez les fonctions ou les fonctions membres qui acceptent/retournent l'une de ses instanciations incomplètes:
void f1(X<int>);
X<int> f2();
Définissez des modèles de fonction ou des modèles de fonction de membre qui acceptent/renvoient des pointeurs/références au type incomplet (mais sans utiliser ses membres):
template <typename T>
void f3(X<T>*, X<T>&) {}
template <typename T>
X<T>& f4(X<T>& in) { return in; }
template <typename T>
X<T>* f5(X<T>* in) { return in; }
Définissez les fonctions ou méthodes qui acceptent/renvoient les pointeurs/références à l’une de ses instanciations incomplètes (mais sans utiliser ses membres):
void f3(X<int>*, X<int>&) {}
X<int>& f4(X<int>& in) { return in; }
X<int>* f5(X<int>* in) { return in; }
Utilisez-le comme classe de base d'une autre classe de modèle
template <typename T>
class Foo : X<T> {} // OK as long as X is defined before
// Foo is instantiated.
Foo<int> a1; // Compiler error.
template <typename T> struct X {};
Foo<int> a2; // OK since X is now defined.
Utilisez-le pour déclarer un membre d'un autre modèle de classe:
template <typename T>
class Foo {
X<T> m; // OK as long as X is defined before
// Foo is instantiated.
};
Foo<int> a1; // Compiler error.
template <typename T> struct X {};
Foo<int> a2; // OK since X is now defined.
Définir des modèles de fonction ou des méthodes utilisant ce type
template <typename T>
void f1(X<T> x) {} // OK if X is defined before calling f1
template <typename T>
X<T> f2(){return X<T>(); } // OK if X is defined before calling f2
void test1()
{
f1(X<int>()); // Compiler error
f2<int>(); // Compiler error
}
template <typename T> struct X {};
void test2()
{
f1(X<int>()); // OK since X is defined now
f2<int>(); // OK since X is defined now
}
Ce que vous ne pouvez pas faire avec un type incomplet:
Utilisez l'une de ses instanciations comme classe de base
class Foo : X<int> {} // compiler error!
Utilisez l'une de ses instanciations pour déclarer un membre:
class Foo {
X<int> m; // compiler error!
};
Définir des fonctions ou méthodes à l'aide de l'une de ses instanciations
void f1(X<int> x) {} // compiler error!
X<int> f2() {return X<int>(); } // compiler error!
Utiliser les méthodes ou champs de l'une de ses instanciations, en essayant en fait de déréférencer une variable de type incomplet
class Foo {
X<int>* m;
void method()
{
m->someMethod(); // compiler error!
int i = m->someField; // compiler error!
}
};
Créer des instanciations explicites du modèle de classe
template struct X<int>;
Dans un fichier dans lequel vous utilisez uniquement le pointeur ou la référence à une classe. Et aucune fonction membre/membre ne doit être invoquée à l'aide de ces pointeurs/références.
avec class Foo;
// déclaration de transfert
Nous pouvons déclarer des données membres de type Foo * ou Foo &.
Nous pouvons déclarer (mais pas définir) des fonctions avec des arguments et/ou des valeurs de retour de type Foo.
Nous pouvons déclarer des membres de données statiques de type Foo. En effet, les membres de données statiques sont définis en dehors de la définition de classe.
J'écris ceci comme une réponse séparée plutôt que comme un commentaire parce que je ne suis pas d'accord avec la réponse de Luc Touraille, non pas pour des raisons de légalité mais pour un logiciel robuste et le risque d'une mauvaise interprétation.
Plus précisément, j'ai un problème avec le contrat implicite de ce que vous attendez des utilisateurs de votre interface.
Si vous renvoyez ou acceptez des types de référence, vous dites simplement qu'ils peuvent passer par un pointeur ou une référence qu'ils n'auraient peut-être connu que par le biais d'une déclaration forward.
Lorsque vous renvoyez un type incomplet X f2();
, vous indiquez que votre appelant doit possède la spécification de type complète de X. Il en a besoin pour créer l'objet LHS ou temporaire sur le site de l'appel. .
De même, si vous acceptez un type incomplet, l'appelant doit avoir construit l'objet qui est le paramètre. Même si cet objet a été renvoyé sous forme d'un autre type incomplet d'une fonction, le site d'appel a besoin de la déclaration complète. c'est à dire.:
class X; // forward for two legal declarations
X returnsX();
void XAcceptor(X);
XAcepptor( returnsX() ); // X declaration needs to be known here
Je pense qu'il existe un principe important selon lequel un en-tête doit fournir suffisamment d'informations pour pouvoir être utilisé sans dépendance nécessitant d'autres en-têtes. Cela signifie que l'en-tête devrait pouvoir être inclus dans une unité de compilation sans provoquer d'erreur de compilation lorsque vous utilisez les fonctions déclarées.
sauf
Si cette dépendance externe est souhaitée comportement. Au lieu d’utiliser la compilation conditionnelle, vous pourriez avoir l’obligation bien documentée de fournir leur propre en-tête déclarant X. C’est une alternative à l’utilisation de #ifdefs et peut être un moyen utile d’introduire des simulacres ou d’autres variantes.
La distinction importante étant certaines techniques de gabarit pour lesquelles vous n'êtes explicitement PAS attendu à les instancier, citées juste pour que personne ne soit sournois avec moi.
La règle générale que je suis est de ne pas inclure de fichier d'en-tête sauf si je suis obligé de le faire. Donc, à moins que je stocke l'objet d'une classe en tant que variable membre de ma classe, je ne l'inclurai pas, j'utiliserai simplement la déclaration forward.
Tant que vous n'avez pas besoin de la définition (pointeurs et références), vous pouvez vous en sortir avec les déclarations en aval. C'est pourquoi vous les voyez généralement dans les en-têtes, tandis que les fichiers d'implémentation extraient généralement l'en-tête des définitions appropriées.
Comme, Luc Touraille a déjà très bien expliqué où utiliser et non utiliser la déclaration en aval de la classe.
Je vais juste ajouter à cela pourquoi nous devons l'utiliser.
Nous devrions utiliser la déclaration Forward chaque fois que possible pour éviter l'injection de dépendance non souhaitée.
Comme les fichiers d'en-tête #include
sont ajoutés à plusieurs fichiers, par conséquent, si nous ajoutons un en-tête dans un autre fichier d'en-tête, il ajoutera une injection de dépendance indésirable dans diverses parties du code source, ce qui peut être évité en ajoutant #include
en-tête dans .cpp
fichiers autant que possible plutôt que de les ajouter à un autre fichier d'en-tête et d'utiliser la déclaration de transmission de classe autant que possible dans les fichiers d'en-tête .h
.
Vous souhaiterez généralement utiliser la déclaration anticipée dans un fichier d'en-tête de classes lorsque vous souhaitez utiliser l'autre type (classe) en tant que membre de la classe. Vous ne pouvez pas utiliser les classes déclarées à l'avance méthodes dans le fichier d'en-tête, car C++ ne connaît pas encore la définition de cette classe. C'est logique que vous deviez vous déplacer dans les fichiers .cpp, mais si vous utilisez des fonctions de template, vous devriez les réduire à la partie qui utilise le template et déplacer cette fonction dans l'en-tête.
Supposons que la déclaration forward obtienne votre code à compiler (obj est créé). Cependant, la création de liens (création exe) ne sera pas réussie à moins que les définitions ne soient trouvées.
Je veux juste ajouter une chose importante que vous pouvez faire avec une classe transférée non mentionnée dans la réponse de Luc Touraille.
Ce que vous pouvez faire avec un type incomplet:
Définissez les fonctions ou méthodes qui acceptent/renvoient les pointeurs/références au type incomplet et transmettent ces pointeurs/références à une autre fonction.
void f6(X*) {}
void f7(X&) {}
void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }
Un module peut passer d'un objet d'une classe déclarée à un autre module.