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

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

Forward declaring an enum in C++

我正在尝试做类似以下的事情:

1
2
3
4
5
enum E;

void Foo(E e);

enum E {A, B, C};

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

澄清2:我这样做是因为我在类中使用私有方法来获取枚举,并且我不希望枚举枚举值 - 例如,我不希望任何人知道E被定义为

1
2
3
enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

因为项目X不是我希望用户了解的东西。

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

至于编译器 - 它是GCC。


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

从ISO C ++标准的第7.2.5节:

The underlying type of an enumeration is an integral type that can represent all the enumerator values defined in the enumeration. It is implementation-defined which integral type is used as the underlying type for an enumeration except that the underlying type shall not be larger than int unless the value of an enumerator cannot fit in an int or unsigned int. If the enumerator-list is empty, the underlying type is as if the enumeration had a single enumerator with value 0. The value of sizeof() applied to an enumeration type, an object of enumeration type, or an enumerator, is the value of sizeof() applied to the underlying type.

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

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


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

1
2
3
4
5
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.


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

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

1
2
3
4
5
6
7
8
9
10
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。


使用C ++声明事物非常有用,因为它可以大大加快编译时间。你可以用C ++转发声明几件事,包括:structclassfunction等......

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

不,你不能。

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

错误。

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

这意味着什么?

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

ISO C ++标准S7.2.5:

The underlying type of an enumeration is an integral type that can represent all the enumerator values defined in the enumeration. It is implementation-defined which integral type is used as the underlying type for an enumeration except that the underlying type shall not be larger than int unless the value of an enumerator cannot fit in an int or unsigned int. If the enumerator-list is empty, the underlying type is as if the enumeration had a single enumerator with value 0. The value of sizeof() applied to an enumeration type, an object of enumeration type, or an enumerator, is the value of sizeof() applied to the underlying type.

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

如果您明确指定enum的类型,该怎么办:

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

那么你可以转发声明你的enum吗?

不,但为什么不呢?

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

资源


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

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

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


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

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

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


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

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

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

1
enum X;

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

1
enum X : int;

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


我这样做:

[在公共标题中]

1
2
3
typedef unsigned long E;

void Foo(E e);

[在内部标题中]

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

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


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

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

1
2
3
4
5
6
7
class A
{
public:
    ...
private:
    void* pImpl;
};

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

1
2
3
4
5
6
7
8
9
class AImpl
{
public:
    AImpl(A* pThis): m_pThis(pThis) {}

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

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

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

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

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


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

1
2
3
4
5
6
7
8
9
#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


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

这里有趣的讨论


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

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

The underlying type of an enumeration
is an integral type that can represent
all the enumerator values defined in
the enumeration. It is
implementation-defined which integral
type is used as the underlying type
for an enumeration except that the
underlying type shall not be larger
than int unless the value of an
enumerator cannot fit in an int or
unsigned int. If the enumerator-list
is empty, the underlying type is as if
the enumeration had a single
enumerator with value 0. The value of
sizeof() applied to an enumeration
type, an object of enumeration type,
or an enumerator, is the value of
sizeof() applied to the underlying
type.

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

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

A class that has been declared but not defined, or an array of unknown size or of
incomplete element type, is an incompletely-defined object type.

A class type (such as"class X") might be incomplete at one point in a translation
unit and complete later on; the type"class X" is the same type at both points. The
declared type of an array object might be an array of incomplete class type and
therefore incomplete; if the class type is completed later on in the translation unit,
the array type becomes complete; the array type at those two points is the same type.
The declared type of an array object might be an array of unknown size and therefore be
incomplete at one point in a translation unit and complete later on; the array types at
those two points ("array of unknown bound of T" and"array of N T") are different
types. The type of a pointer to array of unknown size, or of a type defined by a typedef
declaration to be an array of unknown size, cannot be completed.

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

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


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

  • 以下代码编译好了。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        typedef int myint;
        enum T ;
        void foo(T * tp )
        {
            * tp = (T)0x12345678;
        }
        enum T : char
        {
            A
        };

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

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

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

    • 当看到enum T; VC假设枚举类型T使用默认的4字节int作为基础类型,因此生成的汇编代码为:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        ?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
    线?

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        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
    "
    ,  a.ca[0], a.ca[1], a.ca[2], a.ca[3] );
            return 0;
        }

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

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

    上述关键指令变为:

    mov BYTE PTR [eax],120; 00000078H

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

    请注意,该值不会被覆盖

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

    顺便说一下,基础类型声明的语法与C#中的语法相同。在实践中,我发现通过在与嵌入式系统通信时将底层类型指定为char来保存3个字节是值得的,这是内存有限的。


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

    forward.h:

    1
    2
    3
    4
    5
    namespace type
    {
        class legacy_type;
        typedef const legacy_type& type;
    }

    enum.h:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 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中:

    1
    2
    3
    4
    5
    6
    7
    #include"forward.h"

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

    foo.cc:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #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


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

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

    1
    2
    3
    4
    5
    6
    namespace
    {
       const int FUNCTIONALITY_NORMAL = 0 ;
       const int FUNCTIONALITY_RESTRICTED = 1 ;
       const int FUNCTIONALITY_FOR_PROJECT_X = 2 ;
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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。


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

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


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

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

    尽管编译器关注枚举类型的大小,但是当您转发声明时,枚举的意图会丢失。


    推荐阅读