Quand puis-je utiliser une déclaration à terme?

602

Je cherche la définition du moment où je suis autorisé à faire la déclaration avant 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 passée à la fonction membre par référence, etc.?

Igor Oks
la source
14
Je veux désespérément que cela soit renommé "quand dois- je", et les réponses mises à jour de manière appropriée ...
deworde
12
@deworde Quand vous dites quand "devrait" vous demandez votre avis.
AturSams
@deworde, je crois comprendre que vous souhaitez utiliser des déclarations avancées chaque fois que vous le pouvez, pour améliorer le temps de génération et éviter les références circulaires. La seule exception à laquelle je peux penser est lorsqu'un fichier include contient des typedefs, auquel cas il y a un compromis entre redéfinir le typedef (et risquer qu'il change) et inclure un fichier entier (avec ses inclusions récursives).
Ohad Schneider
@OhadSchneider D'un point de vue pratique, je ne suis pas un grand fan des en-têtes que mon. ÷
deworde
Fondamentalement, vous devez toujours inclure un en-tête différent pour les utiliser (le déclin du paramètre constructeur est un grand coupable ici)
deworde

Réponses:

962

Mettez-vous à la place du compilateur: lorsque vous transmettez un type, le compilateur sait que ce type existe; il ne sait rien de sa taille, de ses membres ou de ses méthodes. C'est pourquoi on l'appelle 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 devrait connaître la disposition 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 qu'un membre est un pointeur ou une référence au type incomplet:

    class Foo {
        X *p;
        X &r;
    };
  • Déclarez les fonctions ou méthodes qui acceptent / renvoient des types incomplets:

    void f1(X);
    X    f2();
  • Définissez des fonctions ou des méthodes qui acceptent / renvoient des 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 à l'aide de ce type

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Utilisez ses méthodes ou champs, 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: si vous pouvez utiliser un type incomplet comme paramètre de modèle dépend de la façon dont le type est utilisé dans le modèle.

Par exemple, std::vector<T>requiert que son paramètre soit un type complet, alors que ce boost::container::vector<T>n'est pas le cas. Parfois, un type complet n'est requis que si vous utilisez certaines fonctions membres; c'est le casstd::unique_ptr<T> par exemple.

Un modèle bien documenté doit indiquer dans sa documentation toutes les exigences de ses paramètres, y compris s'il doit s'agir de types complets ou non.

Luc Touraille
la source
4
Excellente réponse, mais veuillez consulter la mienne ci-dessous pour le point d'ingénierie sur lequel je ne suis pas d'accord. En bref, si vous n'incluez pas d'en-têtes pour les types incomplets que vous acceptez ou renvoyez, vous forcez une dépendance invisible à ce que le consommateur de votre en-tête sache de quels autres il a besoin.
Andy Dent
2
@AndyDent: Vrai, mais le consommateur de l'en-tête n'a besoin que d'inclure les dépendances qu'il utilise réellement, donc cela suit le principe C ++ de "vous ne payez que ce que vous utilisez". Mais en effet, cela peut être gênant pour l'utilisateur qui s'attendrait à ce que l'en-tête soit autonome.
Luc Touraille
8
Cet ensemble de règles ignore un cas très important: vous avez besoin d'un type complet pour instancier la plupart des modèles de la bibliothèque standard. Une attention particulière doit être accordée à cela, car la violation de la règle entraîne un comportement indéfini et ne peut pas provoquer d'erreur de compilation.
James Kanze
12
+1 pour le "mettez-vous à la place du compilateur". J'imagine que le «compilateur étant» ayant une moustache.
PascalVKooten
3
@JesusChrist: Exactement: lorsque vous passez un objet par valeur, le compilateur doit connaître sa taille afin d'effectuer la manipulation de pile appropriée; lors du passage d'un pointeur ou d'une référence, le compilateur n'a pas besoin de la taille ou de la disposition de l'objet, seulement la taille d'une adresse (c'est-à-dire la taille d'un pointeur), qui ne dépend pas du type pointé.
Luc Touraille
45

La règle principale est que vous ne pouvez déclarer que des classes dont la disposition de la mémoire (et donc les fonctions membres et les membres de données) n'ont pas besoin d'être connues dans le fichier que vous déclarez.

Cela exclurait les classes de base et tout sauf les classes utilisées via des références et des pointeurs.

Timo Geusch
la source
6
Presque. Vous pouvez également faire référence à des types incomplets "simples" (c'est-à-dire sans pointeur / référence) en tant que paramètres ou types de retour dans les prototypes de fonctions.
j_random_hacker
Qu'en est-il des classes que je souhaite utiliser en tant que membres d'une classe que je définis dans le fichier d'en-tête? Puis-je les déclarer?
Igor Oks
1
Oui, mais dans ce cas, vous ne pouvez utiliser qu'une référence ou un pointeur vers la classe déclarée vers l'avant. Mais cela vous permet néanmoins d'avoir des membres.
Reunanen
32

Lakos fait la distinction entre l'utilisation des classes

  1. nominatif uniquement (pour lequel une déclaration à terme est suffisante) et
  2. taille (pour laquelle la définition de classe est nécessaire).

Je ne l'ai jamais vu prononcer plus succinctement :)

Marc Mutz - mmutz
la source
2
Qu'est-ce que le nom uniquement?
Boon
4
@Boon: oserais-je le dire ...? Si vous utilisez uniquement le nom de la classe ?
Marc Mutz - mmutz
1
Plus un pour Lakos, Marc
mlvljr
28

En plus des pointeurs et des références à des types incomplets, vous pouvez également déclarer des prototypes de fonctions qui spécifient des paramètres et / ou renvoient des valeurs qui sont des types incomplets. Cependant, vous ne pouvez pas définir une fonction dont le paramètre ou le type de retour est incomplet, sauf s'il s'agit d'un pointeur ou d'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
j_random_hacker
la source
20

Jusqu'à présent, aucune des réponses ne décrit quand on peut utiliser une déclaration directe d'un modèle de classe. Alors, c'est parti.

Un modèle de classe peut être transmis déclaré comme:

template <typename> struct X;

En suivant la structure de la 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 qu'un membre est 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 qu'un membre est un pointeur ou une référence à l'une de ses instanciations incomplètes:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Déclarez des modèles de fonction ou des modèles de fonction membre qui acceptent / renvoient des types incomplets:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Déclarez des fonctions ou des fonctions membres qui acceptent / renvoient 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 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 des fonctions ou des méthodes qui acceptent / renvoient des 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éfinissez des modèles de fonction ou des méthodes à l'aide de 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:

  • Utiliser 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 des 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 les champs d'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>;
R Sahu
la source
2
"Jusqu'à présent, aucune des réponses ne décrit le moment où l'on peut transmettre la déclaration d'un modèle de classe." N'est-ce pas simplement parce que la sémantique de Xet X<int>est exactement la même, et seule la syntaxe de déclaration directe diffère de quelque manière que ce soit, avec toutes les lignes de votre réponse sauf une équivalant à prendre simplement Luc et s/X/X<int>/g? Est-ce vraiment nécessaire? Ou ai-je manqué un petit détail différent? C'est possible, mais j'ai comparé visuellement plusieurs fois et je n'en vois aucun ...
underscore_d
Je vous remercie! Cette modification ajoute une tonne d'informations précieuses. Je vais devoir le lire plusieurs fois pour bien le comprendre ... ou peut-être utiliser la tactique souvent meilleure d'attendre jusqu'à ce que je sois horriblement confus dans le vrai code et que je revienne ici! Je pense que je serai en mesure de l'utiliser pour réduire les dépendances à divers endroits.
underscore_d
4

Dans le fichier dans lequel vous n'utilisez que le pointeur ou la référence à une classe, et aucune fonction membre / membre ne doit être invoquée par le biais de ce pointeur / référence.

avec class Foo;// déclaration avant

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 données statiques membres de type Foo. En effet, les membres de données statiques sont définis en dehors de la définition de classe.

yesraaj
la source
4

J'écris ceci comme une réponse distincte plutôt que comme un commentaire car 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 danger 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 ne peuvent à leur tour connaître que par le biais d'une déclaration directe.

Lorsque vous renvoyez un type incomplet, X f2();vous dites que votre appelant doit avoir la spécification de type complète de X. Il en a besoin pour créer le LHS ou l'objet 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é comme un autre type incomplet à partir 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 y a un principe important selon lequel un en-tête doit fournir suffisamment d'informations pour l'utiliser sans une dépendance nécessitant d'autres en-têtes. Cela signifie que l'en-tête doit pouvoir être inclus dans une unité de compilation sans provoquer d'erreur de compilation lorsque vous utilisez les fonctions qu'il déclare.

Sauf

  1. Si cette dépendance externe est le comportement souhaité . Au lieu d'utiliser la compilation conditionnelle, vous pourriez avoir une exigence bien documentée pour qu'ils fournissent 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 simulations ou d'autres variantes.

  2. La distinction importante étant certaines techniques de modèle où vous n'êtes explicitement PAS censé les instancier, mentionnées juste pour que quelqu'un ne devienne pas sarcastique avec moi.

Andy Dent
la source
1
"Je pense qu'il existe un principe important qu'un en-tête doit fournir suffisamment d'informations pour l'utiliser sans qu'une dépendance ne nécessite d'autres en-têtes." - un autre problème est mentionné dans un commentaire d'Adrian McCarthy sur la réponse de Naveen. Cela fournit une bonne raison de ne pas suivre votre principe "devrait fournir suffisamment d'informations à utiliser" même pour les types actuellement non basés sur des modèles.
Tony Delroy
4
Vous parlez du moment où vous devriez (ou ne devriez pas) utiliser la déclaration directe. Ce n'est absolument pas le but de cette question. Il s'agit de connaître les possibilités techniques lorsque (par exemple) vous souhaitez résoudre un problème de dépendance circulaire.
JonnyJD
2
I disagree with Luc Touraille's answerAlors écrivez-lui un commentaire, y compris un lien vers un article de blog si vous avez besoin de la longueur. Cela ne répond pas à la question posée. Si tout le monde pensait que des questions sur le fonctionnement de X justifiaient des réponses en désaccord avec X ou en débattant des limites dans lesquelles nous devrions restreindre notre liberté d'utiliser X - nous n'aurions presque pas de vraies réponses.
underscore_d
3

La règle générale que je respecte est de ne pas inclure de fichier d'en-tête, sauf si je le dois. Donc, sauf si 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 directe.

Naveen
la source
2
Cela rompt l'encapsulation et rend le code fragile. Pour ce faire, vous devez savoir si le type est un typedef ou une classe pour un modèle de classe avec des paramètres de modèle par défaut, et si l'implémentation change un jour, vous devrez mettre à jour chaque fois que vous avez utilisé une déclaration directe.
Adrian McCarthy
@AdrianMcCarthy a raison, et une solution raisonnable consiste à avoir un en-tête de déclaration directe qui est inclus par l'en-tête dont il déclare le contenu, qui devrait être détenu / maintenu / expédié par la personne qui possède également cet en-tête. Par exemple: l'en-tête de bibliothèque iosfwd Standard, qui contient des déclarations avancées du contenu iostream.
Tony Delroy
3

Tant que vous n'avez pas besoin de la définition (pensez aux pointeurs et aux références), vous pouvez vous en tirer avec des déclarations avancées. C'est pourquoi la plupart du temps vous les voyez dans les en-têtes alors que les fichiers d'implémentation tirent généralement l'en-tête pour la ou les définitions appropriées.

dirkgently
la source
0

Vous souhaiterez généralement utiliser la déclaration directe 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 méthodes de classes déclarées dans le fichier d'en-tête car C ++ ne connaît pas encore la définition de cette classe à ce stade. C'est la logique que vous devez déplacer dans les fichiers .cpp, mais si vous utilisez des fonctions de modèle, vous devez les réduire à la partie qui utilise le modèle et déplacer cette fonction dans l'en-tête.

Patrick Glandien
la source
Cela n'a aucun sens. On ne peut pas avoir un membre d'un type incomplet. Toute déclaration de classe doit fournir tout ce que tous les utilisateurs doivent savoir sur sa taille et sa disposition. Sa taille inclut les tailles de tous ses membres non statiques. La déclaration préalable d'un membre ne laisse aux utilisateurs aucune idée de sa taille.
underscore_d
0

Supposons que la déclaration avant obtiendra votre code à compiler (obj est créé). La liaison cependant (création d'exe) ne réussira que si les définitions sont trouvées.

Sesh
la source
2
Pourquoi 2 personnes ont-elles voté pour cela? Vous ne parlez pas de quoi parle la question. Vous voulez dire la déclaration normale - et non directe - des fonctions . La question concerne la déclaration anticipée des classes . Comme vous l'avez dit "la déclaration vers l'avant obtiendra votre code à compiler", faites-moi une faveur: compilez class A; class B { A a; }; int main(){}et faites-moi savoir comment cela se passe. Bien sûr, il ne se compilera pas. Toutes les bonnes réponses ici expliquent pourquoi et les contextes précis et limités dans lesquels la déclaration à terme est valable. Vous avez plutôt écrit ceci sur quelque chose de totalement différent.
underscore_d
0

Je veux juste ajouter une chose importante que vous pouvez faire avec une classe renvoyée non mentionnée dans la réponse de Luc Touraille.

Ce que vous pouvez faire avec un type incomplet:

Définissez des fonctions ou des méthodes qui acceptent / renvoient des pointeurs / références au type incomplet et transfèrent 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 à travers un objet d'une classe déclarée directe vers un autre module.

Gentil homme
la source
"une classe transmise" et "une classe déclarée avancée" peuvent être confondus avec deux choses très différentes. Ce que vous avez écrit découle directement des concepts implicites dans la réponse de Luc, donc même si cela aurait fait un bon commentaire en ajoutant une clarification manifeste, je ne suis pas sûr que cela justifie une réponse.
underscore_d
0

Comme, Luc Touraille a déjà très bien expliqué où utiliser et ne pas utiliser la déclaration directe de la classe.

J'ajouterai simplement à cela pourquoi nous devons l'utiliser.

Nous devrions utiliser la déclaration Forward dans la mesure du possible pour éviter l'injection de dépendance indésirable.

Comme #includeles fichiers d'en-tête sont ajoutés sur plusieurs fichiers, par conséquent, si nous ajoutons un en-tête dans un autre fichier d'en-tête, cela ajoutera une injection de dépendance indésirable dans diverses parties du code source, ce qui peut être évité en ajoutant #includedans la .cppmesure du possible un en-tête dans des fichiers plutôt qu'en ajoutant à un autre fichier d'en-tête et utilisez la déclaration de classe vers l'avant dans la mesure du possible dans les .hfichiers d' en-tête .

A 786
la source