在C++中前向声明枚举

305
我正在尝试做类似以下的事情:
enum E;

void Foo(E e);

enum E {A, B, C};

编译器拒绝了这个。我在谷歌上快速查了一下,大家的共识似乎是“你不能这样做”。为什么会这样呢?

澄清2:我这样做是因为我在一个类中有私有方法,这些方法接受上述的枚举类型,而我不想让枚举的值暴露出来。例如,我不希望任何人知道E被定义为

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

由于项目X不是我希望用户了解的内容。
所以,我想提前声明枚举,这样我就可以将私有方法放在头文件中,在cpp文件中内部声明枚举,并将构建的库文件和头文件分发给其他人。
至于编译器,使用的是GCC。

这么多年过去了,不知怎么的StackOverflow又把我吸引回来了 ;) 作为一条事后建议 - 千万别这样做,尤其是在你描述的场景中。我更喜欢定义一个抽象接口并将其暴露给用户,将枚举定义和所有其他实现细节保留在内部实现中,这些细节对我的其他人都不可见,从而使我可以随时进行任何操作,并完全控制用户何时看到任何内容。 - RnR
如果你阅读了已接受的答案之后,这是完全可能的,因为C++11支持此功能。 - fuzzyTew
19个回答

268

C++11之后,可以进行枚举类型的前向声明。以前,不能前向声明枚举类型的原因是,枚举类型的大小取决于其内容。只要应用程序指定了枚举的大小,就可以进行前向声明:

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

1
这个特性有没有编译器支持?GCC 4.5好像没有它 :( - rubenvb
4
Visual C++ 11 (2012)也是如此。详情请见http://blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx - knatten
我在寻找enum32_t,而你的回答是:enum XXX : uint32_t {a,b,c}; - Fantastory
我认为作用域枚举(enum class)是在C++11中实现的?如果是这样,在C++0X中它们是如何合法的? - Terrabits
2
C++0x是C++11的工作名称,@Terrabits,在它被正式标准化之前。逻辑是,如果一个功能已知(或高度可能)包含在更新的标准中,那么在官方发布标准之前使用该功能的使用倾向于使用工作名称。(例如,在2011年正式标准化之前支持C++11功能的编译器具有C++0x支持,在正式标准化之前支持C++17功能的编译器具有C++1z支持,而目前(2019年)支持C++20功能的编译器具有C++2a支持。) - Justin Time - Reinstate Monica
显示剩余3条评论

230
枚举类型无法进行前向声明的原因是,没有了解到其值,编译器就无法知道枚举变量所需的存储空间。C++编译器允许根据需要容纳所有指定值的大小来指定实际存储空间。如果只有前向声明可见,翻译单元就不知道选择了什么存储大小 - 可能是char、int或其他类型。

来自 ISO C++ 标准的第 7.2.5 节:

枚举类型的 基础类型 是一个整数类型,可以表示枚举中定义的所有枚举值。除非枚举值无法适应 intunsigned int,否则使用哪种整数类型作为枚举的基础类型是由实现定义的。如果 枚举值列表 为空,则假定枚举具有单个枚举器,其值为 0。应用于枚举类型、枚举类型对象或枚举器的 sizeof() 的值等于应用于基础类型的 sizeof() 的值。

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

更新:

在 C++0X 中,提出并接受了一种用于前向声明枚举类型的语法。您可以在 Forward declaration of enumerations (rev.3) 中查看该提案。


34
你的推理不可能正确--否则,为什么你可以提前声明" class C;",并声明一个函数原型,该原型接受或返回C,在完全定义C之前? - j_random_hacker
121
在类完全定义之前,你不能使用它 - 你只能使用指向该类的指针或引用,因为它们的大小和操作方式不取决于该类是什么。 - RnR
27
类对象的引用或指针的大小由编译器确定,与实际对象大小无关 - 它是指针和引用的大小。枚举是一个对象,编译器需要它的大小以便访问正确的存储位置。 - KJAWolf
19
如果我们能够前向声明枚举,就像我们可以使用类一样,那么在逻辑上,我们应该能够声明指向枚举的指针/引用。只不过你通常不会经常处理指向枚举的指针 :) - Pavel Minaev
24
我知道这个讨论已经很久了,但我必须和 @j_random_hacker 保持一致:问题不在于指向或引用不完整类型,而在于在声明中使用不完整类型。由于执行 struct S; void foo(S s); 是合法的(注意 foo 只被声明了而没有定义),那么我们为什么不能执行 enum E; void foo(E e); 呢?在两种情况下都不需要大小信息。 - Luc Touraille
显示剩余19条评论

96

在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也支持此功能;在早期版本中,它具有某种非标准支持,我还没有弄清楚 - 我发现一些建议简单的前向声明是合法的,但您的情况可能会有所不同。


6
+1是因为这是唯一一个提到你需要在声明和定义中都声明类型的答案。 - turoni
我相信早期 MSVC 中的部分支持是从 C++/CLI 的 enum class 作为 C++ 扩展进行回溯的(在 C++11 的不同 enum class 之前),至少如果我记得正确的话。编译器允许您指定枚举的基础类型,但不支持 enum class 或前向声明的枚举,并警告您使用枚举器的枚举范围是非标准扩展。我记得它的工作方式与在 C++11 中指定基础类型大致相同,只是更烦人,因为您必须抑制警告。 - Justin Time - Reinstate Monica
这就是我在发现可以使用#ifndef宏来定义枚举只有一次的同时,删除额外的头文件以便两个源文件共享一个枚举时所寻找的。这是一个更好的解决方案。 - Advent
1
@Advent 注意,为了避免多次编译头文件,通常最好使用#pragma once而不是#ifndef包含保护 - 它只需一行而不是三行,并且更少容易出错。 - Tom
@Tom 是的,我最近从 #ifdef 切换到了 #pragma once。 - Advent

31

在C++中,前向声明很有用,因为它可以大大加快编译时间。你可以前向声明多种东西,包括:structclassfunction等。

但是,你能在C++中前向声明enum吗?

不行。

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

错了。

在C++中,没有像C#中(int)那样的默认类型用于enum。在C++中,enum类型将由编译器确定为适合enum值范围的任何类型。

这是什么意思?

这意味着,直到你定义了enum中的所有值,才能完全确定enum的底层类型。这意味着你不能分离enum的声明和定义。因此,在C++中无法前向声明enum

ISO C++标准S7.2.5:

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

你可以使用sizeof操作符来确定C++中枚举类型的大小。枚举类型的大小等于其底层类型的大小。通过这种方式,你可以猜测编译器为enum使用的类型。

如果你明确指定enum类型,会发生什么呢?

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

你能预先声明你的enum吗?

不行。但是为什么不行呢?

在当前的C++标准中,指定enum类型实际上并不是必须的。这是VC++的扩展功能。不过在C++0x中将成为标准。

来源


23
这个回答现在已经过时了几年。 - Tom
5
时间使我们所有人都变得愚蠢。你的评论现在已经过时了几年,答案则是十年前的! - pjcard

15

[我的答案是错误的,但我将其保留在此处,因为评论很有用]。

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

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


2
如果你的推理是正确的,那么同样的推理也意味着类类型的前向声明不能用于创建指向这些类型的指针——但它们可以。 - j_random_hacker
7
推理正确。具体情况是在 sizeof(char*) > sizeof(int*) 的平台上。两者都可以作为枚举的底层类型,取决于范围。类没有底层类型,因此比喻是错误的。 - MSalters
3
例子:"struct S { int x; };" 现在,sizeof(S*)必须等于任何其他指向结构体的指针的大小,因为C++允许在定义S之前声明和使用这样的指针... - j_random_hacker
1
@MSalters:在一个sizeof(char*) > sizeof(int*)的平台上,对于这个特定的结构体使用这样一个“全尺寸”指针可能是低效的,但它极大地简化了编码 - 对于枚举类型也可以做到完全相同的事情,而且应该这样做。 - j_random_hacker
4
指向数据和指向函数的指针可能具有不同大小,但我相当确定数据指针必须进行往返转换(将其强制转换为另一种数据指针类型,然后再转换回原始类型,仍需要有效),这意味着所有数据指针的大小相同。 - Ben Voigt
显示剩余2条评论

8
确实不存在枚举的前向声明。由于枚举的定义不包含任何可能依赖于使用该枚举的其他代码的代码,因此通常在首次声明时完全定义枚举是没有问题的。
如果您的枚举只被私有成员函数使用,您可以通过将枚举本身作为该类的私有成员来实现封装。枚举仍然必须在声明点完全定义,也就是在类定义内部。但是,这并不比在那里声明私有成员函数更大的问题,并且不会比那更糟糕地暴露实现细节。
如果您需要更深度的隐藏您的实现细节,您可以将其分解为一个抽象接口,仅包含纯虚函数,以及实现(继承)接口的具体完全隐蔽的类。类实例的创建可以由工厂或接口的静态成员函数处理。这样,甚至真正的类名,更不用说它的私有函数了,也不会被暴露出来。

6
我只是想指出,实际上的原因是在前向声明后枚举的大小尚未知晓。使用前向声明结构体可以使得能够从引用了前向声明结构体定义本身的地方传递指针或引用对象。
前向声明枚举并不是很有用,因为人们希望能够按值传递枚举。你甚至不能有一个指向它的指针,因为我最近被告知一些平台使用指向char的指针与指向int或long的指针大小不同。所以这都取决于枚举的内容。
当前的C++标准明确禁止像这样做:
enum X;

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

enum X : int;

这被称为“不透明”枚举声明。您甚至可以在以下代码中使用按值传递的 X。其枚举器可以在稍后重新声明枚举时定义。请参见当前工作草案中的7.2节。


5
我会这样做:
[在公共头文件中]
typedef unsigned long E;

void Foo(E e);

[在内部头文件中]

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

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

2
当然,这意味着(A)E和Econtent的类型不同,以及(B)在LP64系统上,sizeof(E)= 2 * sizeof(EContent)。微不足道的修复:ULONG_MAX,更易读。 - MSalters

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
结构体AImpl;结构体A { 私有:AImpl* pImpl; }; - Roger Pate

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接