web-dev-qa-db-fra.com

Forward déclarant une énumération en C ++

J'essaie de faire quelque chose comme ce qui suit:

enum E;

void Foo(E e);

enum E {A, B, C};

que le compilateur rejette. J'ai jeté un coup d'œil sur Google et le consensus semble être "vous ne pouvez pas le faire", mais je ne comprends pas pourquoi. Quelqu'un peut-il expliquer?

Clarification 2: Je fais cela car j'ai des méthodes privées dans une classe qui prend dit enum, et je ne veux pas que les valeurs de l'énum soient exposées - ainsi, par exemple, je ne veux pas que quiconque sache que E est défini comme

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

car le projet X n’est pas quelque chose que je veux que mes utilisateurs connaissent.

Donc, je voulais transmettre déclarer l'énumération afin de pouvoir mettre les méthodes privées dans le fichier d'en-tête, déclarer l'énum en interne dans le cpp, et distribuer le fichier de bibliothèque généré et l'en-tête aux personnes.

En ce qui concerne le compilateur - c'est GCC.

253
szevvy

La raison pour laquelle l'énum ne peut pas être déclarée est que, sans connaître les valeurs, le compilateur ne peut pas connaître le stockage requis pour la variable enum. Les compilateurs C++ sont autorisés à spécifier l'espace de stockage réel en fonction de la taille nécessaire pour contenir toutes les valeurs spécifiées. Si seule la déclaration directe est visible, l'unité de traduction ne peut pas savoir quelle taille de stockage aura été choisie - il peut s'agir d'un caractère, d'un entier, ou de quelque chose d'autre.


À partir de la section 7.2.5 de la norme ISO C++:

Le type sous-jacent d'une énumération est un type intégral pouvant représenter toutes les valeurs d'énumérateur définies dans l'énumération. Le type intégral utilisé est le type sous-jacent pour une énumération, à la condition que le type sous-jacent ne soit pas supérieur à int sauf si la valeur d'un énumérateur ne peut pas tenir dans un int ou unsigned int. Si la liste d'énumérateurs est vide, le type sous-jacent est comme si l'énumération avait un seul énumérateur de valeur 0. La valeur de sizeof() appliqué à un type d'énumération, à un objet de type énumération ou à un énumérateur, est la valeur de sizeof() appliquée au type sous-jacent.

Puisque l'appelant de la fonction doit connaître la taille des paramètres pour configurer correctement la pile d'appels, le nombre d'énumérations dans une liste d'énumération doit être connu avant le prototype de fonction.

Mise à jour: dans C++ 0X, une syntaxe pour la déclaration préalable de types enum a été proposée et acceptée. Vous pouvez voir la proposition sur le site http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2764.pdf

206
KJAWolf

La déclaration en aval des énumérations est également possible en C++ 0x. Auparavant, la raison pour laquelle les types d'énumération ne pouvaient pas être déclarés était parce que la taille de l'énumération dépend de son contenu. Tant que la taille de l'énumération est spécifiée par l'application, elle peut être déclarée en aval:

enum Enum1;                   //Illegal in C++ and C++0x; no size is explicitly specified.
enum Enum2 : unsigned int;    //Legal in C++0x.
enum class Enum3;             //Legal in C++0x, because enum class declarations have a default type of "int".
enum class Enum4: unsigned int; //Legal C++0x.
enum Enum2 : unsigned short;  //Illegal in C++0x, because Enum2 was previously declared with a different type.
188
user119017

J'ajoute ici une réponse actualisée, compte tenu de l'évolution récente.

Vous pouvez transmettre-déclarer une énumération en C++ 11, à condition de déclarer son type de stockage en même temps. La syntaxe ressemble à ceci:

enum E : short;
void foo(E e);

....

enum E : short
{
    VALUE_1,
    VALUE_2,
    ....
}

En fait, si la fonction ne fait jamais référence aux valeurs de l'énumération, vous n'avez pas du tout besoin de la déclaration complète à ce stade.

Ceci est supporté par G ++ 4.6 et les versions ultérieures (-std=c++0x ou -std=c++11 dans les versions plus récentes). Visual C++ 2013 prend en charge cela; dans les versions précédentes, il disposait d'une sorte de support non standard que je n'avais pas encore découvert - j'ai trouvé une suggestion selon laquelle une simple déclaration anticipée est légale, mais YMMV.

73
Tom

Déclarer des choses en C++ est très utile car il accélère considérablement le temps de compilation . Vous pouvez transmettre plusieurs déclarations en C++, notamment: struct, class, function, etc ...

Mais pouvez-vous transmettre déclarer une enum en C++?

Non tu ne peux pas.

Mais pourquoi ne pas le permettre? Si cela était autorisé, vous pourriez définir votre type enum dans votre fichier d'en-tête et vos valeurs enum dans votre fichier source. On dirait qu'il devrait être permis non?

Faux.

En C++, il n'y a pas de type par défaut pour enum comme il en existe en C # (int). En C++, le compilateur déterminera que votre type enum correspondra à la plage de valeurs que vous avez pour votre enum.

Qu'est-ce que ça veut dire?

Cela signifie que le type sous-jacent de votre enum ne peut pas être entièrement déterminé tant que toutes les valeurs du enum ne sont pas définies. Vous ne pouvez pas séparer la déclaration et la définition de votre enum. Et par conséquent, vous ne pouvez pas transmettre déclarer une enum en C++.

La norme ISO C++ S7.2.5:

Le type sous-jacent d'une énumération est un type intégral pouvant représenter toutes les valeurs d'énumérateur définies dans l'énumération. Le type intégral utilisé est le type sous-jacent pour une énumération, à la condition que le type sous-jacent ne soit pas supérieur à int sauf si la valeur d'un énumérateur ne peut pas tenir dans un int ou unsigned int. Si la liste d'énumérateurs est vide, le type sous-jacent est comme si l'énumération avait un énumérateur unique avec la valeur 0. La valeur de sizeof() appliquée à un type d'énumération, à un objet de type énumération ou à un énumérateur est valeur de sizeof() appliquée au type sous-jacent.

Vous pouvez déterminer la taille d'un type énuméré en C++ à l'aide de l'opérateur sizeof. La taille du type énuméré est la taille de son type sous-jacent. De cette façon, vous pouvez deviner le type utilisé par votre compilateur pour votre enum.

Que faire si vous spécifiez le type de votre enum explicitement comme ceci:

enum Color : char { Red=0, Green=1, Blue=2};
assert(sizeof Color == 1);

Pouvez-vous ensuite déclarer votre enum?

Non mais pourquoi pas?

Spécifier le type d'un enum ne fait pas partie du standard C++ actuel. C'est une extension VC++. Cela fera cependant partie de C++ 0x.

Source

30
Brian R. Bondy

[Ma réponse est fausse, mais je l'ai laissée ici parce que les commentaires sont utiles].

La déclaration en aval des énumérations est non standard, car les pointeurs vers différents types d’énumérations ne sont pas garantis de la même taille. Le compilateur peut avoir besoin de voir la définition pour savoir quels pointeurs de taille peuvent être utilisés avec ce type.

En pratique, du moins sur tous les compilateurs populaires, les pointeurs sur les énumérations ont une taille cohérente. La déclaration en aval des énumérations est fournie sous forme d'extension de langage par Visual C++, par exemple.

13
James Hopkin

Il n’existe en effet pas de déclaration anticipée d’enum. Comme la définition d'un enum ne contient aucun code pouvant dépendre d'un autre code utilisant l'enum, il n'est généralement pas problématique de définir l'enum complètement lorsque vous le déclarez pour la première fois.

Si la seule utilisation de votre enum est par des fonctions membres privées, vous pouvez implémenter l'encapsulation en ayant l'énum lui-même en tant que membre privé de cette classe. L'énumération doit encore être entièrement définie au moment de la déclaration, c'est-à-dire dans la définition de la classe. Cependant, le fait d’y déclarer des fonctions de membre privées n’est pas un problème plus grave et n’est pas pire en ce qui concerne les internes d’implémentation.

Si vous avez besoin d'un degré de dissimulation plus profond pour les détails de votre implémentation, vous pouvez le diviser en une interface abstraite composée uniquement de fonctions virtuelles pures et d'une classe concrète, complètement dissimulée, implémentant (héritant) l'interface. La création d'instances de classe peut être gérée par une fabrique ou une fonction membre statique de l'interface. De cette façon, même le vrai nom de classe, sans parler de ses fonctions privées, ne sera pas exposé.

7
Alexey Feldgendler

Juste en notant que la raison en fait est que la taille de l'énum n'est pas encore connue après la déclaration anticipée. Eh bien, vous utilisez la déclaration directe d'une structure pour pouvoir passer un pointeur ou faire référence à un objet à partir d'un endroit référencé dans la définition de structure déclarée directe elle-même.

Déclarer en avant une énumération ne serait pas très utile, car on souhaiterait pouvoir la transmettre par valeur. Vous ne pouvez même pas avoir un pointeur dessus, car on m'a récemment dit que certaines plates-formes utilisaient des pointeurs de taille différente pour char, que pour int ou long. Tout dépend donc du contenu de l'énum.

La norme C++ actuelle interdit explicitement de faire quelque chose comme

enum X;

(dans 7.1.5.3/1). Mais la prochaine norme C++ en raison de l’année prochaine permet ce qui suit, ce qui m’a convaincu le problème a à voir avec le type sous-jacent:

enum X : int;

C'est ce qu'on appelle une déclaration enum "opaque". Vous pouvez même utiliser X par valeur dans le code suivant. Et ses énumérateurs peuvent être définis ultérieurement dans une nouvelle déclaration ultérieure de l'énumération. Voir 7.2 dans le brouillon actuel.

5

Je le ferais de cette façon:

[dans l'en-tête public]

typedef unsigned long E;

void Foo(E e);

[dans l'en-tête interne]

enum Econtent { FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X,
  FORCE_32BIT = 0xFFFFFFFF };

En ajoutant FORCE_32BIT, nous nous assurons que Econtent compile sur une longue durée, il est donc interchangeable avec E.

4
Laurie Cheers

On dirait qu'il ne peut pas être déclaré en avant dans GCC!

Discussion intéressante ici

2
prakash

Vous pouvez envelopper l'énumération dans une structure, en ajoutant des constructeurs et des conversions de types, et en avant déclarer la structure à la place.

#define ENUM_CLASS(NAME, TYPE, VALUES...) \
struct NAME { \
    enum e { VALUES }; \
    explicit NAME(TYPE v) : val(v) {} \
    NAME(e v) : val(v) {} \
    operator e() const { return e(val); } \
    private:\
        TYPE val; \
}

Cela semble fonctionner: http://ideone.com/TYtP2

2
Leszek Swirski

Si vous ne voulez vraiment pas que votre enum apparaisse dans votre fichier d’en-tête ET s’assure qu’il n’est utilisé que par des méthodes privées, une solution peut être l’application du principe pimpl.

C'est une technique qui permet de cacher les internes de la classe dans les en-têtes en déclarant simplement:

class A 
{
public:
    ...
private:
    void* pImpl;
};

Ensuite, dans votre fichier d'implémentation (cpp), vous déclarez une classe qui sera la représentation des internes.

class AImpl
{
public:
    AImpl(A* pThis): m_pThis(pThis) {}

    ... all private methods here ...
private:
    A* m_pThis;
};

Vous devez créer dynamiquement l'implémentation dans le constructeur de la classe et la supprimer dans le destructeur. Lorsque vous implémentez une méthode publique, vous devez utiliser:

((AImpl*)pImpl)->PrivateMethod();

Il existe des avantages pour l'utilisation de pimpl. L'un d'entre eux est de découpler votre en-tête de classe de son implémentation. Inutile de recompiler les autres classes lorsque vous modifiez l'implémentation d'une classe. Une autre est que cela accélère votre temps de compilation parce que vos en-têtes sont si simples.

Mais c'est difficile à utiliser, vous devriez donc vraiment vous demander si le simple fait de déclarer votre enum comme privé dans l'en-tête est un problème.

2
Vincent Robert

Dans mes projets, j'ai adopté la technique Namespace-Bound Enumeration pour gérer les enums des composants hérités et tiers. Voici un exemple:

forward.h:

namespace type
{
    class legacy_type;
    typedef const legacy_type& type;
}

enum.h:

// May be defined here or pulled in via #include.
namespace legacy
{
    enum evil { x , y, z };
}


namespace type
{
    using legacy::evil;

    class legacy_type
    {
    public:
        legacy_type(evil e)
            : e_(e)
        {}

        operator evil() const
        {
            return e_;
        }

    private:
        evil e_;
    };
}

foo.h:

#include "forward.h"

class foo
{
public:
    void f(type::type t);
};

foo.cc:

#include "foo.h"

#include <iostream>
#include "enum.h"

void foo::f(type::type t)
{
    switch (t)
    {
        case legacy::x:
            std::cout << "x" << std::endl;
            break;
        case legacy::y:
            std::cout << "y" << std::endl;
            break;
        case legacy::z:
            std::cout << "z" << std::endl;
            break;
        default:
            std::cout << "default" << std::endl;
    }
}

main.cc:

#include "foo.h"
#include "enum.h"

int main()
{
    foo fu;
    fu.f(legacy::x);

    return 0;
}

Notez que l'en-tête foo.h ne doit rien savoir de legacy::evil. Seuls les fichiers utilisant le type hérité legacy::evil (ici: main.cc) doivent inclure enum.h.

1
mavam

Pour VC, voici le test sur la déclaration aval et la spécification du type sous-jacent:

  1. le code suivant est bien compilé.
 typedef int myint; 
 enum T; 
 void foo (T * tp) 
 {
 * tp = (T) 0x12345678; 
} 
 enum T: char 
 {
 A 
}; 

Mais j'ai eu l'avertissement pour/W4 (/ W3 n'engendre pas cet avertissement)

avertissement C4480: extension non standard utilisée: spécification du type sous-jacent pour l'énumération 'T'

  1. VC (version du compilateur optimiseur C/C++ 32 bits version 15.00.30729.01 pour 80x86 de Microsoft (R) 32) semble défectueux dans le cas ci-dessus:

    • en voyant enum T; VC suppose que le type enum T utilise 4 octets int par défaut comme type sous-jacent. Le code d'assembly généré est donc:
? foo @@ YAXPAW4T @@@ Z PROC; foo 
; Fichier e:\work\c_cpp\cpp_snippet.cpp 
; Ligne 13 
 Appuyez sur ebp 
 Mov ebp, esp 
; Ligne 14 
 Mov eax, DWORD PTR _tp $ [ebp] 
 Mov DWORD PTR [eax], 305419896; 12345678H 
; Ligne 15 
 Pop ebp 
 Ret 0 
? Foo @@ YAXPAW4T @@@ Z ENDP; foo 

Le code d'assemblage ci-dessus est extrait directement de /Fatest.asm, pas de ma supposition personnelle. Voyez-vous le mov DWORD PTR [eax], 305419896; Ligne 12345678H?

l'extrait de code suivant le prouve:

 int principal (int argc, char * argv) 
 {
 union {
 char ca [4]; 
 T t; 
} a; 
 a.ca [0] = a.ca [1] = a. [ca [2] = a.ca [3] = 1; 
 foo (& a. t); 
 printf ("% # x,% # x,% # x,% # x\n", a.ca [0], a.ca [1], a.ca [2] , a.ca [3]); 
 renvoyer 0; 
} 

le résultat est: 0x78, 0x56, 0x34, 0x12

  • après avoir supprimé la déclaration en avant de l'énumération T et passé la définition de la fonction foo après la définition de l'énumération T: le résultat est OK:

l'instruction clé ci-dessus devient:

mov BYTE PTR [eax], 120; 00000078H

le résultat final est: 0x78, 0x1, 0x1, 0x1

Notez que la valeur n'est pas écrasée

Donc, l'utilisation de la déclaration directe d'enum dans VC est considérée comme nuisible.

BTW, pour ne pas surprendre, la syntaxe de déclaration du type sous-jacent est la même que celle en C #. En pratique, j'ai trouvé intéressant d'économiser 3 octets en spécifiant le type sous-jacent char lorsque vous parlez au système intégré, qui est limité en mémoire.

1
zhaorufei

Il y a eu quelques dissensions depuis que cela a été heurté (en quelque sorte), alors voici quelques extraits pertinents de la norme. Les recherches montrent que la norme ne définit pas vraiment la déclaration anticipée, ni ne stipule explicitement que les énumérations peuvent ou ne peuvent pas être déclarées.

Tout d’abord, à partir de dcl.enum, section 7.2:

Le type sous-jacent d'une énumération est un type intégral pouvant représenter toutes les valeurs d'énumérateur définies dans l'énumération. Le type intégral utilisé comme type sous-jacent pour une énumération est défini par l'implémentation, sauf que le type sous-jacent ne doit pas être supérieur à int, à moins que la valeur d'un énumérateur ne puisse tenir dans un int ou un unsigned int. Si la liste d'énumérateurs est vide, le type sous-jacent est comme si l'énumération avait un seul énumérateur avec la valeur 0. La valeur de sizeof () appliquée à un type d'énumération, un objet de type énumération ou un énumérateur est la valeur de sizeof () appliqué au type sous-jacent.

Ainsi, le type sous-jacent d'une énumération est défini par l'implémentation, avec une restriction mineure.

Ensuite, nous passons à la section "Types incomplets" (3.9), qui est à peu près aussi proche que nous en arrivons à toute norme sur les déclarations à terme:

Une classe déclarée mais non définie, ou un tableau de taille inconnue ou de type d'élément incomplet, est un type d'objet incomplètement défini.

Un type de classe (tel que "classe X") peut être incomplet à un moment donné dans une unité de traduction et se terminer plus tard; le type "classe X" est le même type aux deux points. Le type déclaré d'un objet tableau peut être un tableau de type classe incomplet et donc incomplet; si le type de classe est complété ultérieurement dans l'unité de traduction, le type de tableau devient complet; le type de tableau à ces deux points est le même type. Le type déclaré d'un objet tableau peut être un tableau de taille inconnue et donc être incomplet à un moment donné dans une unité de traduction et se terminer ultérieurement; les types de tableaux à ces deux points ("tableau de bornes inconnues de T" et "tableau de TN") sont des types différents. Le type d'un pointeur sur un tableau de taille inconnue ou d'un type défini par une déclaration typedef comme étant un tableau de taille inconnue ne peut pas être renseigné.

La norme définit donc assez bien les types qui peuvent être déclarés en aval. Enum n'étant pas là, les auteurs de compilateurs considèrent généralement la déclaration en aval comme non autorisée par la norme en raison de la taille variable de son type sous-jacent.

Cela a du sens aussi. Les énumérations sont généralement référencées dans des situations par valeur et le compilateur aurait en effet besoin de connaître la taille de stockage dans ces situations. Comme la taille de stockage est définie par l'implémentation, de nombreux compilateurs peuvent simplement choisir d'utiliser des valeurs 32 bits pour le type sous-jacent de chaque énumération. Il devient alors possible de les déclarer en aval. Une expérience intéressante pourrait consister à essayer de déclarer une enum dans Visual Studio, puis à le forcer à utiliser un type sous-jacent supérieur à sizeof (int), comme expliqué ci-dessus, pour voir ce qui se passe.

1
Dan Olson

Ma solution à votre problème serait soit:

1 - utilisez int au lieu d'enums: Déclarez votre ints dans un espace de noms anonyme de votre fichier CPP (pas dans l'en-tête):

namespace
{
   const int FUNCTIONALITY_NORMAL = 0 ;
   const int FUNCTIONALITY_RESTRICTED = 1 ;
   const int FUNCTIONALITY_FOR_PROJECT_X = 2 ;
}

Comme vos méthodes sont privées, personne ne va jouer avec les données. Vous pouvez même aller plus loin pour tester si quelqu'un vous envoie des données non valides:

namespace
{
   const int FUNCTIONALITY_begin = 0 ;
   const int FUNCTIONALITY_NORMAL = 0 ;
   const int FUNCTIONALITY_RESTRICTED = 1 ;
   const int FUNCTIONALITY_FOR_PROJECT_X = 2 ;
   const int FUNCTIONALITY_end = 3 ;

   bool isFunctionalityCorrect(int i)
   {
      return (i >= FUNCTIONALITY_begin) && (i < FUNCTIONALITY_end) ;
   }
}

2: créer une classe complète avec des instanciations constantes, comme en Java. Forward déclare la classe, puis la définit dans le fichier CPP et instancie uniquement les valeurs de type énumération. J'ai fait quelque chose comme ça en C++, et le résultat n'était pas aussi satisfaisant que souhaité, car il avait besoin de code pour simuler une énumération (construction de la copie, opérateur =, etc.).

3: Comme proposé précédemment, utilisez l'énumération déclarée de manière privée. Malgré le fait qu'un utilisateur verra sa définition complète, il ne pourra ni l'utiliser ni utiliser les méthodes privées. Ainsi, vous serez généralement en mesure de modifier l'énumération et le contenu des méthodes existantes sans avoir à recompiler le code à l'aide de votre classe.

Je suppose que ce serait la solution 3 ou 1.

0
paercebal