我正在尝试执行以下操作:
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。
自 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.
无法前向声明枚举的原因是,在不知道值的情况下,编译器无法知道枚举变量所需的存储空间。允许 C++ 编译器根据包含所有指定值所需的大小来指定实际存储空间。如果可见的只是前向声明,则翻译单元无法知道已选择的存储大小——它可能是 char
、int
或其他东西。
来自 ISO C++ 标准的第 7.2.5 节:
枚举的底层类型是一个整数类型,可以表示枚举中定义的所有枚举值。使用哪种整数类型作为枚举的基础类型由实现定义,除非基础类型不得大于 int,除非枚举数的值不能适合 int 或 unsigned int。如果 enumerator-list 为空,则基础类型就好像该枚举具有一个值为 0 的单个枚举器。应用于枚举类型、枚举类型的对象或枚举器的 sizeof() 的值是sizeof() 应用于基础类型。
由于函数的调用者必须知道参数的大小才能正确设置调用堆栈,因此必须在函数原型之前知道枚举列表中的枚举数。
更新:
在 C++0X 中,已经提出并接受了一种用于前向声明枚举类型的语法。您可以在 Forward declaration of enumerations (rev.3) 查看提案
struct S; void foo(S s);
是合法的(注意 foo
只是声明了,没有定义),那么我们也没有理由不能做 enum E; void foo(E e);
。在这两种情况下,都不需要大小。
你可以在 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 支持这一点;在早期版本中,它有一些我还没有弄清楚的非标准支持——我发现一些建议,简单的前向声明是合法的,但你的里程可能会有所不同。
enum class
作为 C++ 扩展(在 C++11 的不同 enum class
之前)向后移植的,至少如果我没记错的话。编译器允许您指定枚举的基础类型,但不支持 enum class
或前向声明的枚举,并警告您使用枚举范围限定枚举数是非标准扩展。我记得它的工作方式与在 C++11 中指定基础类型大致相同,只是更烦人,因为您必须抑制警告。
在 C++ 中前向声明事物非常有用,因为它dramatically speeds up compilation time。您可以在 C++ 中转发声明几项内容,包括:struct
、class
、function
等...
但是您可以在 C++ 中转发声明 enum
吗?
不,你不能。
但为什么不允许呢?如果允许,您可以在头文件中定义 enum
类型,并在源文件中定义 enum
值。听起来应该被允许,对吧?
错误的。
在 C++ 中,enum
没有像 C# (int) 中那样的默认类型。在 C++ 中,您的 enum
类型将由编译器确定为适合您的 enum
值范围的任何类型。
这意味着什么?
这意味着在您定义了 enum
的所有值之前,无法完全确定您的 enum
的基础类型。这意味着您不能将 enum
的声明和定义分开。因此,您不能在 C++ 中转发声明 enum
。
ISO C++ 标准 S7.2.5:
枚举的底层类型是一个整数类型,可以表示枚举中定义的所有枚举值。使用哪种整数类型作为枚举的基础类型由实现定义,除非基础类型不得大于 int,除非枚举数的值不能适合 int 或 unsigned int。如果 enumerator-list 为空,则基础类型就好像该枚举具有一个值为 0 的单个枚举器。应用于枚举类型、枚举类型的对象或枚举器的 sizeof() 的值是sizeof() 应用于基础类型。
您可以使用 sizeof
运算符确定 C++ 中枚举类型的大小。枚举类型的大小是其基础类型的大小。通过这种方式,您可以猜测您的编译器为您的 enum
使用的类型。
如果您像这样明确指定 enum
的类型会怎样:
enum Color : char { Red=0, Green=1, Blue=2};
assert(sizeof Color == 1);
然后您可以转发声明您的 enum
吗?
不,但为什么不呢?
指定 enum
的类型实际上并不是当前 C++ 标准的一部分。它是一个 VC++ 扩展。不过,它将成为 C++0x 的一部分。
[我的答案是错误的,但我把它留在这里是因为评论很有用]。
前向声明枚举是非标准的,因为不保证指向不同枚举类型的指针大小相同。编译器可能需要查看定义以了解此类型可以使用什么大小的指针。
实际上,至少在所有流行的编译器上,指向枚举的指针是一致的大小。例如,枚举的前向声明由 Visual C++ 作为语言扩展提供。
确实没有 enum 的前向声明之类的东西。由于枚举的定义不包含任何可能依赖于使用枚举的其他代码的代码,因此在首次声明枚举时完全定义枚举通常不是问题。
如果枚举的唯一用途是私有成员函数,则可以通过将枚举本身作为该类的私有成员来实现封装。枚举仍然必须在声明点完全定义,即在类定义中。然而,这并不是一个更大的问题,因为在那里声明私有成员函数,并且不是比这更糟糕的实现内部暴露。
如果你需要对你的实现细节进行更深层次的隐藏,你可以把它分解成一个抽象接口,只包含纯虚函数,和一个具体的、完全隐藏的、实现(继承)接口的类。类实例的创建可以由工厂或接口的静态成员函数处理。这样,即使是真正的类名,更不用说它的私有函数,也不会被暴露。
我只是注意到实际上的原因是在前向声明之后枚举的大小尚不清楚。好吧,您使用结构的前向声明能够传递指针或从前向声明的结构定义本身中引用的位置引用对象。
前向声明枚举不会太有用,因为人们希望能够按值传递枚举。你甚至没有指向它的指针,因为最近有人告诉我一些平台对 char 使用不同大小的指针,而不是对 int 或 long。所以这一切都取决于枚举的内容。
当前的 C++ 标准明确禁止做类似的事情
enum X;
(在 7.1.5.3/1
中)。但是明年发布的下一个 C++ 标准允许以下内容,这让我确信问题实际上与底层类型有关:
enum X : int;
它被称为“不透明”枚举声明。您甚至可以在以下代码中使用 X 按值。并且它的枚举数可以在以后的枚举重新声明中定义。请参阅当前工作草案中的7.2
。
我会这样做:
[在公共标题中]
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 互换。
您可以将枚举包装在一个结构中,添加一些构造函数和类型转换,然后转发声明该结构。
#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
如果您真的不希望您的枚举出现在您的头文件中并且确保它仅由私有方法使用,那么一种解决方案可以是遵循 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 有很多优点。一是它将您的类头与其实现分离,并且在更改一个类实现时不需要重新编译其他类。另一个是加快编译时间,因为您的标头非常简单。
但是使用起来很痛苦,所以你真的应该问问自己,只是在标题中将你的枚举声明为私有是不是很麻烦。
自从这被撞(有点)以来,有一些异议,所以这里有一些来自标准的相关位。研究表明,该标准并没有真正定义前向声明,也没有明确说明枚举可以或不能被前向声明。
首先,来自 dcl.enum,第 7.2 节:
枚举的底层类型是一个整数类型,可以表示枚举中定义的所有枚举值。使用哪种整数类型作为枚举的基础类型由实现定义,除非基础类型不得大于 int,除非枚举数的值不能适合 int 或 unsigned int。如果 enumerator-list 为空,则基础类型就好像该枚举具有一个值为 0 的单个枚举器。应用于枚举类型、枚举类型的对象或枚举器的 sizeof() 的值是sizeof() 应用于基础类型。
所以枚举的底层类型是实现定义的,有一个小的限制。
接下来我们翻到关于“不完整类型”的部分(3.9),这与我们接近任何关于前向声明的标准一样接近:
已声明但未定义的类,或大小未知或元素类型不完整的数组,是未完全定义的对象类型。一个类类型(例如“class X”)可能在翻译单元中的某个时间点不完整,然后在稍后完成;类型“X 类”在两个点上都是相同的类型。数组对象的声明类型可能是一个不完整类类型的数组,因此是不完整的;如果类类型稍后在翻译单元中完成,则数组类型变得完整;这两个点的数组类型是相同的类型。数组对象的声明类型可能是一个未知大小的数组,因此在翻译单元中的某个时间点是不完整的,稍后会完整;这两个点的数组类型(“T 的未知边界数组”和“N T 的数组”)是不同的类型。指向未知大小数组的指针的类型,或由 typedef 声明定义为未知大小数组的类型,无法完成。
因此,标准几乎列出了可以前向声明的类型。枚举不存在,因此编译器作者通常认为由于其基础类型的可变大小,标准不允许前向声明。
这也是有道理的。枚举通常在按值情况下引用,编译器确实需要知道这些情况下的存储大小。由于存储大小是实现定义的,许多编译器可能只选择对每个枚举的底层类型使用 32 位值,此时可以转发声明它们。
一个有趣的实验可能是尝试在 Visual Studio 中向前声明一个枚举,然后强制它使用大于 sizeof(int) 的基础类型,如上所述,看看会发生什么。
在我的项目中,我采用了 Namespace-Bound Enumeration 技术来处理来自旧组件和第 3 方组件的 enum
。这是一个例子:
转发.h:
namespace type
{
class legacy_type;
typedef const legacy_type& type;
}
枚举.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_;
};
}
富.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;
}
}
主.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
。
对于 VC++,这是关于前向声明和指定基础类型的测试:
以下代码编译OK。
typedef int myint; enum T ; void foo(T * tp ) { * tp = (T)0x12345678; } enum T : char { A };
但我收到了 /W4
的警告(/W3
不会引发此警告)
警告 C4480:使用了非标准扩展:为枚举“T”指定基础类型
VC++(Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86)在上述情况下看起来有问题:
当看到枚举 T 时; VC 假设枚举类型 T 使用默认的 4 字节 int 作为底层类型,因此生成的汇编代码为:
?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
去掉 enum T 的前向声明并将函数 foo 的定义移到 enum T 的定义之后:结果是 OK:
上述关键指令变为:
mov BYTE PTR [eax], 120 ; 00000078H
最终结果是:
0x78, 0x1, 0x1, 0x1
请注意,该值不会被覆盖。
因此在 VC++ 中使用 enum 的前向声明被认为是有害的。
顺便说一句,毫不奇怪,底层类型的声明语法与 C# 中的相同。在实践中,我发现在与内存有限的嵌入式系统通信时,通过将底层类型指定为 char 来节省三个字节是值得的。
我对您的问题的解决方案是:
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。
对于 iOS/Mac/Xcode 面临此问题的任何人,
如果您在将 XCode 中的 C/C++ 标头与 Objective-C 集成时遇到此问题,只需将文件的扩展名从 .mm 更改为 .m
因为枚举可以是可变大小的整数大小(编译器决定给定枚举的大小),指向枚举的指针也可以具有不同的大小,因为它是整数类型(字符在某些平台上具有不同大小的指针例如)。
所以编译器甚至不能让你前向声明枚举和用户指向它的指针,因为即使在那里,它也需要枚举的大小。
您定义一个枚举以将类型元素的可能值限制为有限集合。此限制将在编译时强制执行。
向前声明稍后将使用“有限集”这一事实不会增加任何价值:后续代码需要知道可能的值才能从中受益。
尽管编译器关心枚举类型的大小,但当您向前声明它时,枚举的意图就会丢失。