web-dev-qa-db-fra.com

Quand puis-je utiliser une déclaration forward?

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.?

581
Igor Oks

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.

922
Luc Touraille

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.

43
Timo Geusch

Lakos distingue l'utilisation de la classe

  1. in-name-only (pour lequel une déclaration anticipée est suffisante) et
  2. in-size (pour lequel la définition de classe est nécessaire).

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

32
Marc Mutz - mmutz

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
28
j_random_hacker

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>;
    
17
R Sahu

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.

5
yesraaj

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

  1. 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.

  2. 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.

4
Andy Dent

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.

3
Naveen

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.

3
dirkgently

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.

0
A 786

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.

0
Patrick Glandien

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.

0
Sesh

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.

0
Niceman