转发在C ++中声明一个枚举

时间:2008-09-16 11:34:53

标签: c++ enums

我正在尝试做以下事情:

enum E;

void Foo(E e);

enum E {A, B, C};
编译器拒绝的

。我已经快速浏览了一下Google,而且共识似乎是“你做不到”,但我无法理解为什么。谁能解释一下?

澄清2:我这样做是因为我在一个接受所述枚举的类中有私有方法,并且我不希望枚举枚举值 - 所以,例如,我不希望任何人知道E被定义如

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

因为项目X不是我希望用户知道的事情。

所以,我想转发声明枚举,所以我可以将私有方法放在头文件中,在cpp内部声明枚举,并将构建的库文件和标题分发给人。

至于编译器 - 它是GCC。

18 个答案:

答案 0 :(得分:201)

枚举无法向前声明的原因是,在不知道值的情况下,编译器无法知道枚举变量所需的存储空间。允许C ++编译器根据包含指定的所有值所需的大小来指定实际存储空间。如果所有可见的是前向声明,则翻译单元无法知道将选择哪种存储大小 - 它可以是char或int,或其他。


ISO C ++标准第7.2.5节:

  

枚举的基础类型是一个整数类型,可以表示枚举中定义的所有枚举器值。它是实现定义的,其中整数类型用作枚举的基础类型,除非基础类型不应大于int,除非枚举器的值不能适合int或{{ 1}}。如果枚举器列表为空,则基础类型就好像枚举具有值为0的单个枚举器。unsigned int的值应用于枚举类型,枚举类型的对象或枚举器,是应用于基础类型的sizeof()的值。

由于函数的调用者必须知道正确设置调用堆栈的参数大小,因此在函数原型之前必须知道枚举列表中的枚举数。

更新: 在C ++ 0X中,已经提出并接受了用于向前声明枚举类型的语法。您可以在http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2764.pdf

查看提案

答案 1 :(得分:185)

在C ++ 0x中也可以进行枚举的前向声明。以前,枚举类型无法向前声明的原因是因为枚举的大小取决于其内容。只要枚举的大小由应用程序指定,就可以向前声明:

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.

答案 2 :(得分:71)

鉴于最近的发展,我在这里添加了一个最新的答案。

只要您同时声明其存储类型,就可以在C ++ 11中转发声明枚举。语法如下所示:

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

....

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

实际上,如果函数从不引用枚举的值,那么此时根本不需要完整的声明。

G ++ 4.6及更高版本(最新版本中-std=c++0x-std=c++11)支持此功能。 Visual C ++ 2013支持这一点;在早期版本中它有一些我尚未想到的非标准支持 - 我发现一些简单的前向声明是合法的,但是YMMV。

答案 3 :(得分:30)

在C ++中转发声明是非常有用的,因为dramatically speeds up compilation time。您可以使用C ++转发声明一些内容,包括:structclassfunction等...

但你可以转发在C ++中声明enum吗?

不,你不能。

但为什么不允许呢?如果允许,您可以在头文件中定义enum类型,并在源文件中定义enum值。听起来应该被允许吗?

错误。

在C ++中,{C}(int)没有enum的默认类型。在C ++中,编译器将确定enum类型为适合enum值的范围的任何类型。

这是什么意思?

这意味着在定义了enum的所有值之前,无法完全确定enum的基础类型。您无法将enum的声明和定义分开。因此,您无法在C ++中转发声明enum

ISO C ++标准S7.2.5:

  

枚举的基础类型是一个整数类型,可以表示枚举中定义的所有枚举器值。它是实现定义的,其中整数类型用作枚举的基础类型,除非基础类型不应大于int,除非枚举器的值不能适合int或{{ 1}}。如果枚举列表为空,则基本类型是因为如果枚举具有值为0的unsigned int施加到枚举类型,枚举类型的对象,或枚举值的单个枚举,是sizeof()的值应用于基础类型。

您可以使用sizeof()运算符确定C ++中枚举类型的大小。枚举类型的大小是其基础类型的大小。通过这种方式,您可以猜出编译器正在为sizeof使用哪种类型。

如果您明确指定enum的类型,请执行以下操作:

enum

然后你能转发声明你的enum Color : char { Red=0, Green=1, Blue=2}; assert(sizeof Color == 1); 吗?

没有。但为什么不呢?

指定enum的类型实际上并不是当前C ++标准的一部分。它是VC ++扩展。它虽然是C ++ 0x的一部分。

Source

答案 4 :(得分:13)

[我的回答是错误的,但我把它留在了这里因为评论很有用]。

前向声明枚举是非标准的,因为指向不同枚举类型的指针不能保证大小相同。编译器可能需要查看定义以了解可以使用此类型的大小指针。

在实践中,至少在所有流行的编译器上,指向枚举的指针是一致的大小。例如,Visual C ++提供了枚举的前向声明作为语言扩展。

答案 5 :(得分:7)

确实没有枚举的前瞻性声明。由于枚举的定义不包含任何可能依赖于使用枚举的其他代码的代码,因此在您首次声明枚举时通常不会完全定义枚举。

如果枚举的唯一用途是私有成员函数,则可以通过将枚举本身作为该类的私有成员来实现封装。枚举仍然必须在声明点完全定义,即在类定义中。但是,这不是一个更大的问题,因为在那里宣布私有成员函数,并不是更糟糕的实现内部的暴露。

如果你需要更深入地隐藏你的实现细节,你可以将它分解为一个抽象的接口,只包含纯虚函数,以及一个实现(继承)接口的具体的,完全隐藏的类。类实例的创建可以由工厂或接口的静态成员函数处理。这样,即使是真正的类名,更不用说它的私有函数了,也不会暴露出来。

答案 6 :(得分:5)

只是注意到实际的原因是在前向声明后尚未知道枚举的大小。好吧,你使用结构的前向声明来传递一个指针或者从前面声明的结构定义本身引用的地方引用一个对象。

转发声明枚举不会太有用,因为人们希望能够传递enum by-value。你甚至没有指向它的指针,因为我最近被告知一些平台使用不同大小的指针用于char而不是int或long。所以这一切都取决于枚举的内容。

当前的C ++标准明确禁止执行类似

的操作
enum X;

(在7.1.5.3/1中)。但是下一年的下一个C ++标准允许以下内容,这使我确信 与底层类型有关的问题:

enum X : int;

它被称为“不透明”的枚举声明。您甚至可以在以下代码中使用值中的X 。并且稍后可以在枚举的后续重新定义中定义其枚举器。请参阅当前工作草案中的7.2

答案 7 :(得分:4)

我这样做:

[在公共标题中]

typedef unsigned long E;

void Foo(E e);

[在内部标题中]

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

通过添加FORCE_32BIT,我们确保Econtent编译为long,因此它可以与E互换。

答案 8 :(得分:2)

您可以将枚举包装在结构中,添加一些构造函数和类型转换,然后转发声明结构。

#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; \
}

这似乎有效: http://ideone.com/TYtP2

答案 9 :(得分:2)

如果你真的不希望你的枚举出现在你的头文件中并确保它只被私有方法使用,那么一个解决方案可以采用pimpl原则。

这是一种确保通过声明来隐藏标题中的类内部的技术:

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

然后在您的实现文件(cpp)中,声明一个将成为内部表示的类。

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

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

您必须在类构造函数中动态创建实现并在析构函数中将其删除,并且在实现公共方法时,您必须使用:

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

使用pimpl有一些优点,一个是它将类头与其实现分离,在更改一个类实现时无需重新编译其他类。另一个是加快编译时间,因为标题很简单。

但是使用起来很痛苦,所以你应该问自己,如果只是在标题中声明你的枚举是私有的那么麻烦。

答案 10 :(得分:2)

似乎无法在海湾合作委员会中向前宣布!

有趣的讨论here

答案 11 :(得分:1)

有一些不同意见,因为这有点受到冲击(有点),所以这里有一些来自标准的相关位。研究表明,该标准并未真正定义前向声明,也没有明确规定枚举可以或不可以向前声明。

首先,来自dcl.enum,第7.2节:

  

枚举的基础类型   是一种可以代表的整体类型   中定义的所有枚举器值   枚举。它是   实现 - 定义哪个积分   type用作底层类型   列举除了   基础类型不应该更大   比int除非a的值   枚举器不能适合int或   unsigned int。如果是枚举器列表   是空的,底层类型就好像   枚举只有一个   值为0的枚举器。值   sizeof()应用于枚举   type,枚举类型的对象,   或者一个普查员,是值的   sizeof()应用于底层   类型。

因此枚举的基础类型是实现定义的,只有一个小的限制。

接下来我们转到关于“不完整类型”(3.9)的部分,这与我们对前向声明的任何标准一样接近:

  

已声明但未定义的类,或未知大小的数组或   不完整的元素类型,是一个未完全定义的对象类型。

     

类型(例如“类X”)在翻译中的某个点可能不完整   单位并在稍后完成;类型“class X”在两个点都是相同的类型。该   声明的数组对象类型可能是一个不完整类类型的数组   因此不完整;如果稍后在翻译单元中完成课程类型,   数组类型变得完整;这两个点的数组类型是相同的类型。   声明的数组对象类型可能是一个未知大小的数组,因此是   翻译单位的某一点不完整,稍后完成;数组类型在   这两点(“T的未知界限的数组”和“N T的数组”)是不同的   类型。指向未知大小的数组或由typedef定义的类型的指针的类型   声明是一个未知大小的数组,无法完成。

所以,标准几乎列出了可以向前声明的类型。枚举不存在,因此编译器作者通常认为由于其基础类型的可变大小而被标准禁止向前声明。

这也是有道理的。枚举通常在按值的情况下引用,编译器确实需要知道这些情况下的存储大小。由于存储大小是实现定义的,因此许多编译器可能只选择使用32位值作为每个枚举的基础类型,此时可以转发声明它们。一个有趣的实验可能是试图在visual studio中声明一个枚举,然后强制它使用大于sizeof(int)的基础类型,如上所述,看看会发生什么。

答案 12 :(得分:1)

在我的项目中,我采用了Namespace-Bound Enumeration技术来处理来自旧版和第三方组件的enum。这是一个例子:

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;
}

请注意,foo.h标题不必了解legacy::evil。只有使用旧版本legacy::evil(此处为:main.cc)的文件才需要包含enum.h

答案 13 :(得分:1)

对于VC,这是关于前向声明和指定基础类型的测试:

  1. 以下代码编译好了。
  2.     typedef int myint;
        enum T ;
        void foo(T * tp )
        {
            * tp = (T)0x12345678;
        }
        enum T : char
        {
            A
        };
    

    但得到了/ W4的警告(/ W3没有招致此警告)

    警告C4480:使用非标准扩展名:为枚举“T”指定基础类型

    1. VC(Microsoft(R)32位C / C ++优化编译器版本15.00.30729.01 for 80x86) 在上述情况下看起来很麻烦:

      • 看到enum T; VC假定枚举类型T使用默认的4个字节int作为基础类型,因此生成的汇编代码为:
    2.     ?foo@@YAXPAW4T@@@Z PROC                 ; foo
          ; File e:\work\c_cpp\cpp_snippet.cpp
          ; Line 13
              push    ebp
              mov ebp, esp
          ; Line 14
              mov eax, DWORD PTR _tp$[ebp]
              mov DWORD PTR [eax], 305419896      ; 12345678H
          ; Line 15
              pop ebp
              ret 0
          ?foo@@YAXPAW4T@@@Z ENDP                 ; foo
      

      上面的汇编代码是直接从/Fatest.asm中提取的,而不是我个人的猜测。 你看到了吗? mov DWORD PTR [eax],305419896; 12345678H 线?

      以下代码段证明了这一点:

          int main(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] );
              return 0;
          }
      

      结果是: 0x78,0x56,0x34,0x12

      • 删除枚举T的前向声明后,在枚举T的定义后移动函数foo的定义:结果没问题:

      上述关键指令变为:

      mov BYTE PTR [eax],120; 00000078H

      最终结果是: 0x78,0x1,0x1,0x1

      请注意,该值未被覆盖

      因此,在VC中使用enum的前向声明被认为是有害的。

      BTW,毫不奇怪,声明底层类型的语法与它在C#中的语法相同。在实践中,我发现通过在与嵌入式系统通信时将底层类型指定为char来保存3个字节是值得的,这是内存有限的。

答案 14 :(得分:0)

我对你的问题的解决方案是:

1 - 使用int而不是枚举:在CPP文件中的匿名命名空间中声明你的int(不在标题中):

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

由于您的方法是私有的,没有人会弄乱数据。如果有人向您发送无效数据,您甚至可以进一步测试:

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:使用有限的const实例创建一个完整的类,就像在Java中完成一样。转发声明类,然后在CPP文件中定义它,并仅仅实现类似枚举的值。我在C ++中做了类似的事情,结果并不像期望的那样令人满意,因为它需要一些代码来模拟枚举(复制构造,运算符=等)。

3:如前所述,使用私有声明的枚举。尽管用户将看到其完整定义,但它将无法使用它,也无法使用私有方法。因此,您通常可以修改枚举和现有方法的内容,而无需使用您的类重新编译代码。

我的猜测是解决方案3或1。

答案 15 :(得分:0)

对于面对iOS / Mac / Xcode的任何人,

如果在将X / C ++标头与Objective-C集成在一起时遇到此问题,只需将文件的扩展名从.mm更改为.m

答案 16 :(得分:-1)

定义枚举以将类型元素的可能值限制为有限集。此限制将在编译时强制执行。

当前方声明您稍后将使用“有限集”这一事实时,不会添加任何值:后续代码需要知道可能的值才能从中受益。

虽然编译器 关注枚举类型的大小,但是当你转发声明时,枚举的 intent 会丢失。

答案 17 :(得分:-1)

因为枚举可以是不同大小的整数大小(编译器决定给定枚举的大小),指向枚举的指针也可以有不同的大小,因为它是一个整数类型(字符具有不同大小的指针)例如在一些平台上。)

所以编译器甚至不能让你向前声明枚举和用户指向它的指针,因为即使在那里,它也需要枚举的大小。