转发声明C ++中的枚举


263

我正在尝试执行以下操作:

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。


经过这么多年,StackOverflow还是以某种方式吸引了我;)作为事后建议- 尤其不要 在您描述的场景中这样做。我宁愿定义一个抽象接口,并向用户公开此接口,并保留枚举定义和所有其他实现细节以及内部实现,这一点其他人看不到,这使我可以随时随地进行操作,并完全控制用户何时看到任何东西。
RnR 2014年

Answers:


216

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


根据ISO C ++标准的7.2.5节:

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

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

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


29
-1。您的推论是不正确的-否则,为什么允许您向前声明“ C类”;然后在完全定义C之前声明一个接受或返回C的函数原型?
j_random_hacker

112
@j_random:您不能在完全定义一个类之前使用它-您只能使用对该类的指针或引用,这是因为它们的大小和操作方式不取决于该类是什么。
RnR

27
类对象的引用或指针的大小由编译器设置,并且与对象的实际大小无关-它是指针和引用的大小。枚举是一个对象,编译器需要使用它的大小才能访问正确的存储。
KJAWolf

16
从逻辑上讲,如果我们有向前声明的枚举,就可以声明对枚举的指针/引用,就像我们对类所做的那样。只是您不经常处理指向枚举的指针:)
Pavel Minaev 09年

20
我知道这个讨论很久以前就结束了,但是我必须在这里与@j_random_hacker对齐:这里的问题不是关于指针或对不完整类型的引用,而是关于在声明中使用不完整类型。由于这样做是合法的struct S; void foo(S s);(请注意,foo仅声明但未定义),因此没有理由不能做到enum E; void foo(E e);。在这两种情况下,都不需要大小。
卢·图拉耶

198

从C ++ 11开始,可以向前声明枚举。以前,无法向前声明枚举类型的原因是因为枚举的大小取决于其内容。只要应用程序指定了枚举的大小,就可以向前声明:

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.

1
此功能有编译器支持吗?GCC 4.5似乎没有它:(
rubenvb 2010年

4
@rubenvb Visual C ++ 11(2012)同样如此blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx
knatten

我一直在寻找enum32_t并与您的答案一起枚举XXX:uint32_t {a,b,c};
fantastory

我以为范围限定的枚举(枚举类)是在C ++ 11中实现的?如果是这样,那么在C ++ 0X中它们如何合法?
Terrabits

1
在正式将其标准化之前,C ++ 0x是C ++ 11的工作名称@Terrabits。逻辑是,如果已知(或很有可能)某个功能包含在更新的标准中,则在标准正式发布之前对该功能的使用倾向于使用工作名称。(例如,在2011年正式标准化之前支持C ++ 11功能的编译器具有C ++ 0x支持,在正式标准化之前具有C ++ 1z支持的支持C ++ 17功能的编译器和支持C ++ 20功能的编译器现在(2019年)支持C ++ 2a。)
贾斯汀时间-恢复莫妮卡

79

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

您可以在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。


4
+1,因为这是唯一提及您需要在声明以及定义中声明类型的答案。
turoni

我相信,至少在我没有记错的情况下,早期MSVC的部分支持已从C ++ / CLI enum class扩展为C ++扩展(在C ++ 11有所不同之前enum class)。编译器允许您指定枚举的基础类型,但不支持enum class或预先声明枚举,并警告您使用枚举的范围限定枚举数是非标准扩展。我记得它的工作原理与在C ++ 11中指定基础类型大致相同,只是更烦人,因为您必须禁止显示警告。
贾斯汀时间-恢复莫妮卡

30

在C ++中进行正向声明非常有用,因为它可以大大加快编译时间。您可以向前声明用C几件事情++包括:structclassfunction,等...

但是您可以enum在C ++中向前声明一个吗?

不,你不能。

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

错误。

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

那是什么意思?

这意味着,enum直到您拥有已enum定义的所有值,才能完全确定其基础类型。您无法区分哪个人的声明和定义enum。因此,您不能enum在C ++中转发声明。

ISO C ++标准S7.2.5:

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

您可以使用sizeof运算符确定C ++中枚举类型的大小。枚举类型的大小是其基础类型的大小。这样,您就可以猜出编译器正在使用哪种类型enum

如果您这样明确指定类型,该怎么办enum

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

然后,您可以转发声明您的enum吗?

不,为什么不呢?

指定an的类型enum实际上不是当前C ++标准的一部分。它是VC ++扩展。不过它将成为C ++ 0x的一部分。

资源


14
这个答案现在已经过时了几年。
汤姆(Tom)

时间使我们所有人愚弄。您的评论现在已经过时了几年;答案十年!
pjcard

14

[我的回答是错误的,但由于评论有用,所以将其留在此处]。

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

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


2
-1。如果您的推理是正确的,则相同的推理将暗示不能使用类类型的前向声明创建指向这些类型的指针,但是可以。
j_random_hacker 2009年

6
+1。推理是正确的。具体情况是平台,其中sizeof(char *)> sizeof(int *)。两者都可以是枚举的基础类型,具体取决于范围。类没有基础类型,因此类比为假。
MSalters

3
@MSalters:示例:“结构S {int x;};” 现在,的sizeof(S *)必须等于任何其它指针到结构的大小,因为C ++允许这样的指针被声明和前S的定义中所用...
j_random_hacker

1
@MSalters:...在其中sizeof(char *)> sizeof(int *)的平台上,对这种特定结构使用这样的“全尺寸”指针可能效率低下,但它极大地简化了编码-完全一样枚举类型可以并且应该做的事情。
j_random_hacker 2009年

4
指向数据的指针和指向函数的指针可以具有不同的大小,但是我很确定数据指针必须进行往返(转换为另一种数据指针类型,然后返回原始指针,仍然需要工作),这意味着所有数据指针大小相同。
Ben Voigt 2010年

7

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

如果枚举的唯一用途是私有成员函数,则可以通过将枚举本身作为该类的私有成员来实现封装。枚举仍必须在声明时(即在类定义中)完全定义。但是,这不是一个更大的问题,因为在那里声明了私有成员函数,并且也没有比这更糟糕的实现内部原理。

如果您需要对实现细节进行更深层次的隐藏,则可以将其分为一个抽象接口,该接口仅由纯虚函数和一个具体的,完全隐藏的类实现(继承)接口组成。类实例的创建可以由接口的工厂或静态成员函数处理。这样,即使是真正的类名,更不用说它的私有功能,也不会公开。


5

只是指出原因实际上在向前声明后还不知道枚举的大小。好吧,您可以使用结构的前向声明来传递指针或从前向声明的结构定义本身所引用的位置引用对象。

前向声明枚举不会太有用,因为人们希望能够传递枚举值。您甚至都没有指向它的指针,因为最近我被告知某些平台对char使用与int或long不同的大小的指针。因此,这完全取决于枚举的内容。

当前的C ++标准明确禁止做类似的事情

enum X;

(在中7.1.5.3/1)。但接下来的C ++标准,由于明年允许以下,这使我确信实际问题具有与基本型的事:

enum X : int;

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


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互换。


1
当然,这意味着(A)E和Econtent的类型不同,并且(B)在LP64系统上,sizeof(E)= 2 * sizeof(EContent)。简单修复:ULONG_MAX,也更易于阅读。
MSalters 2009年

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有很多优点,其中之一是它可以将类头与实现分离,而在更改一个类的实现时无需重新编译其他类。另一个是因为头很简单,所以可以加快编译时间。

但是使用起来很痛苦,因此您应该真正问自己,是否只是在标头中将枚举声明为私有会带来很多麻烦。


3
struct AImpl; struct A {private:AImpl * pImpl; };

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



1

自从出现这种情况以来,存在一些异议(因此),因此这是标准中的一些相关内容。研究表明,该标准并未真正定义前向声明,也未明确声明枚举可以或不能被前向声明。

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

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

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

接下来,我们转到关于“不完整类型”(3.9)的部分,该部分与关于前向声明的任何标准都差不多:

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

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

因此,该标准几乎列出了可以向前声明的类型。枚举不存在,因此编译器作者通常将前向声明视为该标准所不允许的,因为其基础类型的大小可变。

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


请注意,它明确禁止“ enum foo;”。在7.1.5.3/1中进行(但与所有内容一样,只要编译器警告,它当然仍可以编译此类代码)
Johannes Schaub-litb

感谢您指出这一段,这确实是一个深奥的段落,可能要花我一周的时间来解析它。但是很高兴知道它在那里。
丹·奥尔森,2009年

不用担心。一些标准段落真的很奇怪:)好吧,精心设计的类型说明符既可以指定类型,也可以指定更多内容以使其明确。例如,用“结构X”代替“ X”,或者用“枚举Y”代替“ Y”。您需要使用它来断言某种类型。
Johannes Schaub-litb

因此您可以像这样使用它:“ class X * foo;” 如果X尚未向前声明。或“ typename X :: foo”以消除歧义。或“类链接obj;” 如果在同一作用域中有一个函数“链接”将使具有相同名称的类蒙上阴影。
Johannes Schaub-litb

在3.4.4中,它说如果某些非类型名称隐藏了类型名称,则使用它们。这是最常用的地方,除了像“ X类”这样的前向声明之外;(这里是声明的唯一组成部分)。它在这里以非模板的方式谈论它们。但是,14.6 / 3列出了它们在模板中的用法。
Johannes Schaub-litb

1

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

  1. 下面的代码编译正常。
    typedef int myint;
    枚举T;
    无效foo(T * tp)
    {
        * tp =(T)0x12345678;
    }
    枚举T:char
    {
        一个
    };

但是得到了/ W4的警告(/ W3不会引发此警告)

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

  1. 在上述情况下,VC(Microsoft(R)32位C / C ++优化编译器版本15.00.30729.01(对于80x86))看起来有问题:

    • 当看到枚举T时;VC假定枚举类型T使用默认的4字节int作为基础类型,因此生成的汇编代码为:
    ?foo @@ YAXPAW4T @@@ Z PROC; 富
    ; 文件e:\ work \ c_cpp \ cpp_snippet.cpp
    ; 13号线
        推息
        mov ebp,esp
    ; 14号线
        mov eax,DWORD PTR _tp $ [ebp]
        mov DWORD PTR [eax],305419896; 12345678H
    ; 15号线
        流行音乐
        ret 0
    ?foo @@ YAXPAW4T @@@ ZENDP; 富

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

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

    int main(int argc,char * argv)
    {
        工会{
            char ca [4];
            吨
        }一个;
        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]) ;
        返回0;
    }

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

  • 在删除枚举T的前向声明并在枚举T的定义之后移动函数foo的定义之后:结果正常:

上面的关键说明变为:

mov BYTE PTR [eax],120;00000078H

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

请注意该值不会被覆盖

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

顺便说一句,不足为奇的是,用于声明基础类型的语法与C#中的语法相同。在实践中,我发现值得与嵌入式系统对话时将底层类型指定为char来节省3个字节,这是受内存限制的。


1

在我的项目中,我采用了命名空间绑定枚举技术来处理enum旧版和第三方组件中的。这是一个例子:

forward.h:

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

枚举

// 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


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。


-1

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

因此,编译器甚至不能让您向前声明枚举并向其指向用户,因为即使在那儿,它也需要枚举的大小。


-1

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

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

虽然编译器关注的枚举类型的大小,意图当你向前声明它枚举的丢失。


1
不,后续代码不必知道此值是否有用-尤其是,如果后续代码仅仅是采用或返回枚举参数的函数原型,则类型的大小并不重要。在此处使用前向声明可以删除构建依赖关系,从而加快编译速度。
j_random_hacker 2009年

你是对的。目的不是服从值,而是服从类型。用0x枚举类型解决。
xtofl,2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.