Вперед, объявляя перечисление в C++

Я пытаюсь сделать что-то вроде следующего:

enum E;

void Foo(E e);

enum E {A, B, C};

который компилятор отклоняет. Я быстро посмотрел на Google, и консенсус, кажется, "вы не можете сделать это", но я не могу понять, почему. Кто-нибудь может объяснить?

Пояснение 2: Я делаю это, поскольку у меня есть закрытые методы в классе, которые принимают указанное перечисление, и я не хочу, чтобы значения перечисления отображались, поэтому, например, я не хочу, чтобы кто-нибудь знал, что E определяется как

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

поскольку проект X - это не то, о чем я хочу, чтобы мои пользователи знали.

Итак, я хотел переслать объявление enum, чтобы я мог поместить приватные методы в файл заголовка, объявить enum внутри cpp и распространить файл и заголовок встроенной библиотеки людям.

Что касается компилятора - это GCC.

20 ответов

Решение

Причина, по которой перечисление не может быть объявлено заблаговременно, заключается в том, что, не зная значений, компилятор не может узнать объем памяти, необходимый для переменной enum. Компиляторам C++ разрешено указывать фактическое пространство хранения на основе размера, необходимого для хранения всех указанных значений. Если все, что видно, это предварительное объявление, модуль перевода не может знать, какой размер хранилища будет выбран - это может быть char, int или что-то еще.


Из раздела 7.2.5 стандарта ISO C++:

Базовый тип перечисления является целочисленным типом, который может представлять все значения перечислителя, определенные в перечислении. Определяется реализацией, какой интегральный тип используется в качестве базового типа для перечисления, за исключением того, что базовый тип не должен быть больше, чем int если значение перечислителя не может вписаться в int или же unsigned int, Если список-перечислитель пуст, базовый тип такой, как если бы перечисление имело единственный перечислитель со значением 0. Значение sizeof() применяется к типу перечисления, объекту типа перечисления или перечислителю, является значением sizeof() применяется к базовому типу.

Поскольку вызывающая функция должна знать размеры параметров для правильной настройки стека вызовов, число перечислений в списке перечислений должно быть известно до прототипа функции.

Обновление: в C++0X был предложен и принят синтаксис для объявляющих заранее типов enum. Вы можете увидеть это предложение по адресу http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2764.pdf

Прямое объявление перечислений также возможно в C++0x. Ранее причина, по которой типы перечислений не могли быть объявлены вперед, заключается в том, что размер перечисления зависит от его содержимого. Пока размер перечисления определяется приложением, оно может быть объявлено заранее:

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, если вы одновременно объявляете его тип хранения. Синтаксис выглядит так:

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++, включая: struct, class, function, так далее...

Но можете ли вы объявить enum в С ++?

Нет, ты не можешь.

Но почему бы не позволить это? Если бы это было разрешено, вы могли бы определить свой enum введите свой заголовочный файл, и ваш enum значения в вашем исходном файле. Похоже, это должно быть разрешено правильно?

Неправильно.

В C++ нет типа по умолчанию для enum как есть в C# (int). В С ++ твой enum тип будет определяться компилятором как любой тип, который будет соответствовать диапазону значений, которые у вас есть для вашего enum,

Что это значит?

Это означает, что ваш enumбазовый тип не может быть полностью определен, пока у вас не будут все значения enum определены. Какого человека вы не можете разделить декларацию и определение вашего enum, И поэтому вы не можете переслать объявить enum в C++.

Стандарт ISO C++ S7.2.5:

Базовый тип перечисления является целочисленным типом, который может представлять все значения перечислителя, определенные в перечислении. Определяется реализацией, какой интегральный тип используется в качестве базового типа для перечисления, за исключением того, что базовый тип не должен быть больше, чем int если значение перечислителя не может вписаться в int или же unsigned int, Если список-перечислитель пуст, базовый тип такой, как если бы перечисление имело единственный перечислитель со значением 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, хотя.

Источник

[Мой ответ неправильный, но я оставил его здесь, потому что комментарии полезны].

Прямое объявление перечислений не является стандартным, потому что указатели на разные типы перечислений не гарантируют одинаковый размер. Компилятору может понадобиться увидеть определение, чтобы узнать, какие указатели размера могут использоваться с этим типом.

На практике, по крайней мере, на всех популярных компиляторах указатели на перечисления имеют одинаковый размер. Например, в Visual C++ прямое объявление перечислений предоставляется как расширение языка.

На самом деле не существует такой вещи, как предварительная декларация enum. Поскольку определение enum не содержит кода, который мог бы зависеть от другого кода, использующего enum, обычно нет проблем с полным определением 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.

Кажется, это не может быть заранее объявлено в GCC!

Интересная дискуссия здесь

Если вы действительно не хотите, чтобы ваш enum появлялся в вашем заголовочном файле и гарантировал, что он используется только частными методами, тогда одним из решений может быть использование принципа 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, во-первых, он отделяет заголовок вашего класса от его реализации, нет необходимости перекомпилировать другие классы при изменении реализации одного класса. Другое - это ускоряет ваше время компиляции, потому что ваши заголовки очень просты.

Но это неудобно в использовании, поэтому вы должны спросить себя, не является ли проблема просто объявлением вашего enum как частного в заголовке.

Вы можете обернуть перечисление в структуру, добавив некоторые конструкторы и преобразования типов, и вместо этого объявить структуру вперед.

#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:

Базовый тип перечисления является целочисленным типом, который может представлять все значения перечислителя, определенные в перечислении. Определяется реализацией, какой целочисленный тип используется в качестве базового типа для перечисления, за исключением того, что базовый тип не должен быть больше, чем int, если только значение перечислителя не может поместиться в int или unsigned int. Если список перечислителя пуст, базовый тип выглядит так, как если бы перечисление имело единственный перечислитель со значением 0. Значение sizeof(), примененное к типу перечисления, объекту типа перечисления или перечислителю, является значением sizeof() применяется к базовому типу.

Таким образом, базовый тип перечисления определяется реализацией с одним небольшим ограничением.

Далее мы переходим к разделу "неполные типы" (3.9), который примерно так же близок, как мы подходим к любому стандарту на предварительные объявления:

Класс, который был объявлен, но не определен, или массив неизвестного размера или с неполным типом элемента, является не полностью определенным типом объекта.

Тип класса (такой как "класс X") может быть неполным в одной точке в единице перевода и завершаться позже; тип "класс X" является одинаковым типом в обеих точках. Объявленный тип объекта массива может быть массивом неполного типа класса и, следовательно, неполным; если тип класса завершается позже в модуле перевода, тип массива становится завершенным; тип массива в этих двух точках одинаковый. Объявленный тип объекта массива может быть массивом неизвестного размера и, следовательно, быть неполным в одной точке единицы преобразования и завершаться позже; типы массивов в этих двух точках ("массив неизвестной границы T" и "массив N T") - это разные типы. Тип указателя на массив неизвестного размера или тип, определенный объявлением typedef как массив неизвестного размера, не может быть завершен.

Таким образом, стандарт в значительной степени изложил типы, которые могут быть объявлены заранее. Enum там не было, поэтому авторы компилятора обычно считают предварительное объявление запрещенным стандартом из-за переменного размера его базового типа.

Это тоже имеет смысл. На перечисления обычно ссылаются в ситуациях с побочными значениями, и компилятору действительно нужно знать размер хранилища в этих ситуациях. Поскольку размер хранилища определяется реализацией, многие компиляторы могут просто использовать 32-битные значения для базового типа каждого перечисления, после чего становится возможным их прямое объявление. Интересным экспериментом может быть попытка объявить enum в визуальной студии, а затем заставить его использовать базовый тип, больший чем sizeof(int), как объяснено выше, чтобы увидеть, что происходит.

В своих проектах я использовал метод Перечисления, привязанного к пространству имен, чтобы иметь дело с 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 - используйте int вместо enums: объявите свои int в анонимном пространстве имен в вашем файле CPP (не в заголовке):

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 и создать только значения, подобные enum. Я сделал что-то подобное в C++, и результат оказался не таким удовлетворительным, как хотелось бы, так как требовался некоторый код для имитации перечисления (конструкция копирования, оператор = и т. Д.).

3: Как предложено ранее, используйте объявленный в частном порядке enum. Несмотря на то, что пользователь увидит его полное определение, он не сможет ни использовать его, ни использовать приватные методы. Таким образом, вы обычно сможете изменять перечисление и содержимое существующих методов без необходимости перекомпиляции кода с использованием вашего класса.

Я думаю, будет решение 3 или 1.

Для VC вот тест на предварительное объявление и определение базового типа:

  1. следующий код скомпилирован нормально
    typedef int myint;
    enum T;
    void foo(T * tp)
    {
        * tp = (T)0x12345678;
    }
    enum T: char
    {
        
    };

Но получил предупреждение для /W4(/W3 не несет это предупреждение)

предупреждение C4480: используется нестандартное расширение: указание базового типа для перечисления 'T'

  1. VC (32-разрядный оптимизирующий компилятор C/C++ версии 15.00.30729.01 для 80x86) выглядит некорректно в приведенном выше случае:

    • при просмотре enum T; VC предполагает, что тип перечисления T использует 4 байта по умолчанию int как базовый тип, поэтому сгенерированный код сборки:
? foo @@ YAXPAW4T @@@ Z PROC; Foo; Файл e:\work\c_cpp\cpp_snippet.cpp; Линия 13
        толчок
        мов ебп, эсп; Линия 14
        mov eax, DWORD PTR _tp$[ebp]
        mov DWORD PTR [eax], 305419896; 12345678H; Строка 15
        поп ебп
        рет 0?foo@@YAXPAW4T@@@Z ENDP; Foo

Приведенный выше код сборки извлечен непосредственно из /Fatest.asm, а не мое личное предположение. Вы видите mov DWORD PTR [eax], 305419896; 12345678H линия?

следующий фрагмент кода доказывает это:

    int main (int argc, char * argv)
    {
        союз {
            char ca[4];
            Т т;
        } А;
        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]);
        вернуть 0;
    }

результат: 0x78, 0x56, 0x34, 0x12

  • после удаления предварительного объявления enum T и перемещения определения функции foo после определения enum T: результат в порядке:

вышеуказанная ключевая инструкция становится:

mov BYTE PTR [eax], 120; 00000078H

конечный результат: 0x78, 0x1, 0x1, 0x1

Обратите внимание, что значение не перезаписывается

Поэтому использование форвард-декларации enum в VC считается вредным.

Кстати, не удивительно, что синтаксис для объявления базового типа такой же, как в C#. На практике я обнаружил, что стоит сэкономить 3 байта, указав базовый тип как char при обращении к встроенной системе, которая ограничена в памяти.

Всем, кто сталкивается с этим для iOS/Mac/Xcode,

Если вы столкнулись с этим при интеграции заголовков C/C++ в XCode с Objective-C, просто измените расширение вашего файла с.mm на.m

В ответ на уточнение: если вы используете enum только внутри, почему бы не объявить это внутри класса как private?

Вы определяете перечисление, чтобы ограничить возможные значения элементов типа ограниченным набором. Это ограничение должно применяться во время компиляции.

При прямом объявлении того факта, что позже вы будете использовать "ограниченный набор", не добавляется никакого значения: последующий код должен знать возможные значения, чтобы извлечь из него выгоду.

Хотя компилятор заботится о размере перечислимого типа, цель перечисления теряется, когда вы его объявляете.

Поскольку перечисление может иметь целочисленный размер различного размера (компилятор решает, какой размер имеет данное перечисление), указатель на перечисление также может иметь различный размер, поскольку это целочисленный тип (символы имеют указатели разного размера на некоторых платформах например).

Таким образом, компилятор не может даже позволить вам объявить перечисление вперед и указать на него указатель, потому что даже там ему нужен размер перечисления.

Таким образом, мы можем перенаправить объявление enum

enum A : int;

пожалуйста, обратитесь по ссылке для получения подробной информации.

Другие вопросы по тегам