在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个回答

2
由于这个问题被提出来了,所以在这里列出一些标准中相关的部分。研究表明,标准并没有真正定义前向声明,也没有明确说明枚举类型是否可以进行前向声明。
首先,从dcl.enum(7.2节)中得到以下信息:
枚举的底层类型是一个整数类型,可以表示枚举中定义的所有枚举值。实现定义了使用哪种整数类型作为枚举的底层类型,除非一个枚举的值无法适应int或unsigned int。如果枚举列表为空,则底层类型就像枚举具有单个值0的枚举一样。应用于枚举类型、枚举类型的对象或枚举器的sizeof()的值等于应用于底层类型的sizeof()的值。
因此,枚举的底层类型是实现定义的,只有一个小限制。
接下来我们转到“不完全类型”(3.9)的部分,这是关于前向声明的任何标准的最接近的部分:
已声明但未定义的类,或者未知大小或不完整元素类型的数组,都是不完全定义的对象类型。类类型(如“class X”)可能在翻译单元的某个点上是不完整的,在稍后变得完整;类型“class X”在这两个点上是相同的类型。数组对象的声明类型可能是不完整类类型的数组,因此是不完整的;如果类类型稍后在翻译单元中完成,则数组类型变为完整;这两个点的数组类型是相同的类型。数组对象的声明类型可能是未知大小的数组,因此在翻译单元的某个点上是不完整的,在稍后变得完整;这两个点的数组类型(“T的未知边界数组”和“N T的数组”)是不同的类型。指向未知大小数组的指针的类型,或者由typedef声明定义为未知大小数组的类型,不能完成。
所以,标准几乎列出了可以进行前向声明的类型。枚举不在其中,因此编译器作者通常认为由于其底层类型的可变大小,标准禁止进行前向声明。
这也是有道理的。枚举通常在按值引用的情况下被引用,编译器确实需要在这些情况下知道存储大小。由于存储大小是实现定义的,许多编译器可能会选择使用32位值作为每个枚举的底层类型,此时它们就可以被前向声明。
一个有趣的实验可能是尝试在Visual Studio中前向声明一个枚举,然后强制它使用大于sizeof(int)的底层类型,以查看会发生什么。

请注意,在7.1.5.3/1中明确禁止使用"enum foo;"语句(但是像所有情况一样,只要编译器发出警告,它仍然可以编译这样的代码)。 - Johannes Schaub - litb
感谢指出,那段话非常深奥,我可能需要一周时间来解析它。但知道它的存在很好。 - Dan Olson
别担心,有些标准段落确实很奇怪 :) 一个详细的类型说明符是指你指定了一个类型,但也指定了更多的内容以使其不含糊。例如,“struct X”而不是“X”,或者“enum Y”而不仅仅是“Y”。你需要它来断言某个东西确实是一个类型。 - Johannes Schaub - litb
如果X尚未进行前向声明,则可以使用以下方式:"class X * foo;"。在模板中,为了消除歧义,可以使用"typename X::foo"。如果同一作用域中存在一个名为"link"的函数会遮蔽具有相同名称的类,则可以使用"class link obj;"。 - Johannes Schaub - litb
在3.4.4中,它说如果某些非类型名称隐藏了类型名称,则会使用它们。除了像“class X;”这样的前向声明(这里是声明的唯一组成部分)之外,它们最常用于此处。它在这里讨论了非模板的使用。然而,在14.6/3中列出了它们在模板中的用途。 - Johannes Schaub - litb

1

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

  1. 以下代码可以编译通过。
    typedef int myint;
    enum T ;
    void foo(T * tp )
    {
        * tp = (T)0x12345678;
    }
    enum T : char
    {
        A
    };

但是我在使用/W4/W3不会出现此警告)时得到了警告:

warning C4480: nonstandard extension used: specifying underlying type for enum 'T'

  1. 在上述情况下,VC++(Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86)看起来存在缺陷:
  • 当看到enum 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

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

上述关键指令变为:

mov BYTE PTR [eax], 120; 00000078H

最终结果是:

0x78,0x1,0x1,0x1

请注意,值没有被覆盖。

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

顺便提一下,为了不让你感到惊讶,声明基础类型的语法与 C# 中的语法相同。在实践中,我发现在与内嵌系统通信时,将基础类型指定为 char 可以节省三个字节,这对于内存有限的系统来说非常值得。


1
在我的项目中,我采用了命名空间绑定枚举技术来处理来自遗留和第三方组件的enum。以下是一个示例:

forward.h:

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

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

1

在GCC中似乎无法进行前向声明!

这里有一个有趣的讨论。


这个链接仍然有效,但你能在回答中总结一下吗? - undefined

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++中做了类似的事情,结果并不如预期那样令人满意,因为它需要一些代码来模拟枚举(复制构造函数,operator =等)。

3:如之前所提议的,使用私有声明的枚举。尽管用户将看到其完整定义,但它将无法使用它,也无法使用私有方法。因此,您通常可以修改枚举和现有方法的内容,而无需重新编译使用您的类的代码。

我的猜测是解决方案3或1。


0

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

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


1
这是什么解释?为什么它能够工作? - Peter Mortensen

-1

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

当提前声明您将在后面使用“有限集合”时,不会增加任何价值:后续代码需要知道可能的值才能从中受益。

尽管编译器确实关心枚举类型的大小,但是当您提前声明它时,枚举的意图就会丢失。


2
不需要后续代码知道这些值才能发挥其作用——特别是,如果后续代码仅是一个函数原型,接受或返回枚举参数,则类型的大小并不重要。在此使用前向声明可以消除构建依赖项,加快编译速度。 - j_random_hacker
你是对的。意图并不是遵循值,而是类型。可以使用0x枚举类型解决问题。 - xtofl

-1

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

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


-2

这样我们就可以前向声明枚举

enum A : int;

详情请参考链接


1
这个额外的“答案”并没有比2009年的现有答案提供更多的信息。 - Thomas Weller

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