【问题标题】:Forward declaring an enum in C++在 C++ 中前向声明枚举
【发布时间】:2010-09-09 10:37:09
【问题描述】:

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

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 引诱我回来 ;) 作为事后建议 - 只是不要这样做 尤其是在场景中你描述。我更愿意定义一个抽象接口并向用户公开这个接口,并将枚举定义和所有其他实现细节与内部实现保持一致,而我这边没有其他人看到,让我可以随时随地做任何事情,并完全控制用户何时看到任何东西。
  • 如果您阅读了已接受的答案,那么自 C++11 以来这是完全可能的。

标签: c++ enums


【解决方案1】:

从 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.

【讨论】:

  • 这个特性有编译器支持吗? GCC 4.5 似乎没有它:(
  • @rubenvb Visual C++ 11 (2012) blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx 也是如此
  • 我在寻找 enum32_t 并且你的答案是 enum XXX : uint32_t {a,b,c};
  • 我认为作用域枚举(枚举类)是在 C++11 中实现的?如果是这样,那么它们在 C++0X 中如何合法?
  • C++0x 在正式标准化之前是 C++11 的工作名称@Terrabits。逻辑是,如果一个特性已知(或极有可能)包含在更新的标准中,那么在标准正式发布之前使用该特性往往会使用工作名称。 (例如,在 2011 年正式标准化之前支持 C++11 特性的编译器有 C++0x 支持,在官方标准化之前支持 C++17 特性的编译器有 C++1z 支持,以及支持 C++20 特性的编译器现在(2019 年)支持 C++2a。)
【解决方案2】:

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


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

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

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

更新:

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

上查看提案

【讨论】:

  • -1。你的推理不可能是正确的——否则,为什么你被允许前向声明“C级”;然后在完全定义 C 之前声明一个接受或返回 C 的函数原型?
  • @j_random:你不能在一个类完全定义之前使用它——你只能使用一个指针或对该类的引用,这是因为它们的大小和操作方式不依赖于什么类是。
  • 类对象的引用或指针的大小由编译器设置,与对象的实际大小无关——它是指针和引用的大小。枚举是一个对象,编译器需要它的大小才能访问正确的存储。
  • 从逻辑上讲,如果我们有前向声明的枚举,它将能够声明指向枚举的指针/引用,就像我们可以使用类一样。只是你不经常处理指向枚举的指针:)
  • 我知道这个讨论很久以前就结束了,但我必须在这里与@j_random_hacker 保持一致:这里的问题不是指针或对不完整类型的引用,而是关于不完整类型的使用在声明中。既然做struct S; void foo(S s); 是合法的(注意foo 只是声明了,没有定义),那么我们也没有理由不能做enum E; void foo(E e);。在这两种情况下,都不需要大小。
【解决方案3】:

你可以在 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 支持这一点;在早期版本中,它有一些我还没有弄清楚的非标准支持 - 我发现一些建议,简单的前向声明是合法的,但你的里程可能会有所不同。

【讨论】:

  • +1,因为这是唯一提到您需要在声明中声明类型以及定义的答案。
  • 我相信早期 MSVC 中的部分支持是从 C++/CLI 的 enum class 作为 C++ 扩展(在 C++11 的不同 enum class 之前)向后移植的,至少如果我没记错的话。编译器允许您指定枚举的基础类型,但不支持 enum class 或前向声明的枚举,并警告您使用枚举范围限定枚举数是非标准扩展。我记得它的工作原理与在 C++11 中指定底层类型大致相同,只是更烦人,因为你必须抑制警告。
【解决方案4】:

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

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

不,你不能。

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

错了。

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

这是什么意思?

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

ISO C++ 标准 S7.2.5:

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

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

如果您像这样明确指定 enum 的类型会怎样:

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

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

没有。但为什么不呢?

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

Source

【讨论】:

  • 这个答案已经过时了好几年了。
  • 时间愚弄了我们所有人。您的评论现在已经过时了几年;十年的答案!
【解决方案5】:

[我的答案是错误的,但我把它留在这里是因为 cmets 很有用]。

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

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

【讨论】:

  • -1。如果你的推理是正确的,那么同样的推理意味着类类型的前向声明不能用于创建指向这些类型的指针——但它们可以。
  • +1。推理是正确的。具体情况是 sizeof(char*) > sizeof(int*) 的平台。两者都可以是枚举的基础类型,具体取决于范围。类没有底层类型,所以类比是错误的。
  • @MSalters:示例:“struct S { int x; };”现在, sizeof (S*) 必须 等于任何其他指向结构的指针的大小,因为 C++ 允许在定义 S...
  • @MSalters: ... 在 sizeof(char*) > sizeof(int*) 的平台上,对这个特定结构使用这样的“全尺寸”指针可能效率低下,但它显着简化了编码——对于枚举类型可以而且应该做同样的事情。
  • 指向数据的指针和指向函数的指针可以是不同的大小,但我相当确定数据指针必须往返(转换为另一种数据指针类型,然后返回原始类型,需要仍然有效),这意味着所有数据指针的大小都相同。
【解决方案6】:

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

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

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

【讨论】:

    【解决方案7】:

    我只是注意到实际上的原因是在前向声明之后枚举的大小尚不清楚。好吧,您使用结构的前向声明能够传递指针或从前向声明的结构定义本身中引用的位置引用对象。

    转发声明一个枚举不会太有用,因为人们希望能够按值传递枚举。你甚至没有指向它的指针,因为最近有人告诉我,一些平台对 char 使用不同大小的指针,而不是 intlong。所以这一切都取决于枚举的内容。

    当前的 C++ 标准明确禁止这样做

    enum X;
    

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

    enum X : int;
    

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

    【讨论】:

      【解决方案8】:

      我会这样做:

      [在公共标头中]

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

      【讨论】:

      • 当然,这意味着 (A) E 和 Econtent 的类型不同,并且 (B) 在 LP64 系统上,sizeof(E) = 2 * sizeof(EContent)。微不足道的修复:ULONG_MAX,也更易于阅读。
      【解决方案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

      【讨论】:

        【解决方案10】:

        如果你真的不希望你的枚举出现在你的头文件中并且确保它只被私有方法使用,那么一种解决方案可以是遵循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 有很多优点。一是它将您的类头与其实现分离,并且在更改一个类实现时不需要重新编译其他类。另一个是加快编译时间,因为您的标头非常简单。

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

        【讨论】:

        • 结构 AImpl;结构 A { 私有:AImpl* pImpl; };
        【解决方案11】:

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

        forward.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_;
            };
        }
        

        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

        【讨论】:

          【解决方案12】:

          似乎不能在 GCC 中前向声明!

          一个有趣的讨论是here

          【讨论】:

            【解决方案13】:

            自从这被撞了(有点)以来,有一些异议,所以这里有一些来自标准的相关部分。研究表明,该标准并没有真正定义前向声明,也没有明确说明枚举可以或不能被前向声明。

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

            枚举的底层类型 是一个整数类型,可以表示 中定义的所有枚举器值 枚举。它是 实现定义哪个积分 type 用作基础类型 对于枚举,除了 底层类型不应更大 比 int 除非值 枚举数不能放入 int 或 无符号整数。如果枚举器列表 为空,底层类型好像 枚举有一个 值为 0 的枚举器。 sizeof() 应用于枚举 type,枚举类型的对象, 或枚举器,是 sizeof() 应用于底层证券 输入。

            所以枚举的底层类型是实现定义的,有一个小的限制。

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

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

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

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

            这也是有道理的。枚举通常在按值情况下引用,编译器确实需要知道这些情况下的存储大小。由于存储大小是实现定义的,许多编译器可能只选择对每个枚举的底层类型使用 32 位值,此时可以转发声明它们。

            一个有趣的实验可能是尝试在 Visual Studio 中向前声明一个枚举,然后强制它使用大于 sizeof(int) 的基础类型,如上所述,看看会发生什么。

            【讨论】:

            • 请注意,它明确禁止“enum foo;”在 7.1.5.3/1 中(但与所有内容一样,只要编译器发出警告,它当然仍然可以编译此类代码)
            • 感谢您指出,这是一个非常深奥的段落,我可能需要一个星期的时间来解析它。但很高兴知道它在那里。
            • 不用担心。一些标准段落真的很奇怪 :) 好吧,详细的类型说明符是您指定类型的地方,但也指定更多内容以使其明确。例如“struct X”而不是“X”,或“enum Y”而不是只有“Y”。你需要它来断言某些东西确实是一种类型。
            • 所以你可以像这样使用它:“class X * foo;”如果 X 尚未前向声明。或模板中的“typename X::foo”用于消歧。或“类链接obj;”如果在同一范围内有一个函数“链接”会影响具有相同名称的类。
            • 在 3.4.4 中,如果某些非类型名称隐藏了类型名称,则使用它们。这是它们最常用的地方,除了像“class X”这样的前向声明; (这里是声明的唯一组成部分)。它在这里用非模板谈论它们。但是,14.6/3 列出了它们在模板中的使用。
            【解决方案14】:

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

            1. 以下代码编译成功。
            typedef int myint; 枚举 T ; 无效 foo(T * tp ) { * tp = (T)0x12345678; } 枚举 T:字符 { 一种 };

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

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

            1. 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 ;富 ;文件 e:\work\c_cpp\cpp_sn-p.cpp ; 13号线 推送ebp mov ebp, esp ;第 14 行 mov eax, DWORD PTR _tp$[ebp] mov DWORD PTR [eax], 305419896 ; 12345678H ; 15号线 流行音乐 0 ?foo@@YAXPAW4T@@@Z ENDP ;富

            上面的汇编代码是直接从/Fatest.asm中提取出来的,不是我个人的猜测。

            你看到了吗

            mov DWORD PTR[eax], 305419896        ; 12345678H
            

            行?

            下面的代码 sn -p 证明了这一点:

            int main(int argc, char *argv) { 联合{ 字符 ca[4]; 吨; }一种; a.ca[0] = a.ca[1] = a.[ca[2] = a.ca[3] = 1; 富(&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的前向声明,将函数foo的定义移到枚举T的定义后:结果OK:

            以上关键指令变为:

            mov BYTE PTR [eax], 120 ; 00000078H

            最终结果是:

            0x78, 0x1, 0x1, 0x1

            注意该值没有被覆盖。

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

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

            【讨论】:

              【解决方案15】:

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

              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。

              【讨论】:

                【解决方案16】:

                对于在 iOS/Mac/Xcode 中遇到此问题的任何人,

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

                【讨论】:

                • 解释是什么?为什么会这样?
                【解决方案17】:

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

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

                【讨论】:

                  【解决方案18】:

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

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

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

                  【讨论】:

                  • 不,后续代码不需要知道它的值才能有用——特别是,如果后续代码只是一个函数原型,采用或返回枚举参数,则类型的大小并不重要.这里使用前向声明可以去除构建依赖,加快编译速度。
                  • 你是对的。目的不是遵守值,而是遵守类型。解决了 0x 枚举类型。
                  猜你喜欢
                  • 2010-10-31
                  • 2011-11-20
                  • 2023-03-27
                  • 1970-01-01
                  • 2013-02-14
                  • 1970-01-01
                  相关资源
                  最近更新 更多