Почему c++11 строго типизированное перечисление не может быть приведено к базовому типу с помощью указателя?

В C++11 мы можем привести строго типизированное перечисление (enum class) к базовому типу. Но, похоже, мы не можем указать на то же самое:

enum class MyEnum : int {};

int main()
{
  MyEnum me;

  int iv = static_cast<int>(me); // works
  int* ip = static_cast<int*>(&me); // "invalid static_cast"
}

Я пытаюсь понять, почему это должно быть: есть ли что-то в механизме перечисления, что затрудняет или бессмысленно поддерживать это? Это простая оплошность в стандарте? Что-то еще?

мне кажется, что если тип перечисления действительно построен поверх интегрального типа, как указано выше, мы должны быть в состоянии не только значения, но и указатели. Мы все еще можем использовать reinterpret_cast<int*> или литье в стиле C, но это больший молоток, чем я думал, нам понадобится.

6 ответов


TL; DR: дизайнеры C++ не любят каламбуров.

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

безопасность типа C++

в C++ в целом типы не связаны, если явно не указано, что они связаны (через наследование). Рассмотрим следующий пример:

class A
{
    double x;
    int y;
};

class B
{
    double x;
    int y;
};

void foo(A* a)
{
    B* b = static_cast<B*>(a); //error
}

хотя A и B имеют одно и то же представление (стандарт даже назвал бы их "типами стандартных макетов"), вы не можете конвертировать между ними без a reinterpret_cast. Аналогично, это также ошибка:

class C
{
public:
    int x;
};

void foo(C* c)
{
    int* intPtr = static_cast<int*>(c); //error
}

даже хотя мы знаем, что единственное в C-это int, и вы можете свободно получить к нему доступ,static_cast не удается. Почему? Явно не указано, что эти типы связаны. C++ был разработан для поддержки объектно-ориентированного программирования, которое обеспечивает различие между композицией и наследованием. Вы можете конвертировать между типами, связанными по наследованию, но не связанными по составу.

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

композиция против наследования

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

  1. строго типизированные перечисления не предназначены для использования в качестве интеграла ценности. Таким образом, отношение "есть-а", обозначаемое наследованием, не подходит.
  2. на самом высоком уровне перечисления предназначены для представления набора дискретных значений. Тот факт, что это реализуется путем присвоения идентификационного номера каждому значению, как правило, не важен (к сожалению, C предоставляет и, таким образом, обеспечивает эту связь).
  3. оглядываясь на предложение, перечисленная причина для позволять определенному основному типу определить размер и обозначение перечисления. Это гораздо больше детали реализации, чем существенная часть перечисления, опять же в пользу композиции.

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


вместо этого посмотрите на это немного по-другому. Ты не можешь!--0--> a long* to int* даже если int и long имеют идентичные базовые представления. По той же причине перечисление, основанное на int пока рассматривается как уникальный, не связанный с int и как таковое требует reinterpret_cast.


перечисление-это отдельный тип (3.9.2) с именованными константами. [...] Каждое перечисление определяет тип, который отличается от всех других типов. [...] Два типа перечисления совместимы с макетом, если они имеют один и тот же базовый тип.

[dcl.перечисление] (§7.2)

базовый тип определяет макет перечисления в памяти, а не его отношение к другим типам в системе типов (как говорит стандарт, это особый тип, своего рода). Указатель на enum : int {} никогда не может неявно преобразовать в int*, так же, как указатель на struct { int i; }; не может, хотя все они выглядят одинаково в памяти.

так почему же неявное преобразование в int работать в первую очередь?

для перечисления, базовый тип-это фиксированные значения перечисление-это значения базового типа. [...] Значение перечислитель или объект типа перечисления unscoped преобразовать в целое число с помощью комплексное продвижение (4.5).

[dcl.перечисление] (§7.2)

таким образом, мы можем присвоить значения перечисления int потому что они имеют тип int. Объект типа перечисления может быть назначен int из-за правил продвижения целое. Кстати, стандарт здесь конкретно указывает, что это верно только для перечислений C-style (unscoped). Это значит, что ты еще нужен static_cast<int> в первой строке вашего примера, но как только вы включите enum class : int на enum : int он будет работать без явного приведения. Еще не повезло с типом указателя.

интегральные акции определяются в стандарте на [усл.пром] (§4.5). Я избавлю вас от подробностей цитирования полного раздела, но важная деталь здесь заключается в том, что все правила там применяются к prvalues типов без указателей, поэтому ничего из этого относится к нашей маленькой проблеме.

окончательный кусок головоломки можно найти в [expr.статический.cast] (§5.2.9), который описывает как static_cast строительство.

значение типа перечисления с областью видимости (7.2) может быть явно преобразовано к интегральному типу.

это объясняет, почему ваш бросок от enum class to int строительство.

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

это связано с @MarkB это: Static-кастинг указателя enum для указателя на int аналогично приведению указателя от одного интегрального типа к другому-даже если оба имеют одинаковый макет памяти под причем значения одного будут неявно преобразовываться в другие по правилам интегральных промо-акций, они все равно несвязанные типы, так что static_cast не будет работать здесь.


Я думаю, что ошибка мышления заключается в том, что

enum class MyEnum : int {};

на самом деле не наследование. Конечно, вы можете сказать MyEnum это int. Однако он отличается от классического наследования, поскольку не все операции, доступные на intможно MyEnum также.

давайте сравним это со следующим: круг-это эллипс. Однако почти всегда было бы неправильно реализовать CirlceShape как наследовать от EllipseShape С не все операции, которые возможны на эллипсах, также возможны для круга. Простой пример-масштабирование формы в X направлении.

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

++*reinterpret_cast<int*>(&me);

это может быть причиной, почему Комитет запрещен static_cast в этом случае. В общем reinterpret_cast считается злом, в то время как static_cast считается в порядке.


ответы на ваши вопросы можно найти в разделе 5.2.9 статические литой в проект стандарта.

поддержки

int iv = static_cast<int>(me);

и

5.2.9 / 9 значение типа перечисления с областью видимости (7.2) может быть явно преобразовано в интегральный тип. Значение не изменяется, если исходное значение может быть представлено указанным типом. В противном случае, результирующее значение неопределенный.

поддержки

me = static_cast<MyEnum>(100);

и

5.2.9 / 10 значение типа integral или перечисления может быть явно преобразовано в тип перечисления. Значение не изменяется, если исходное значение находится в диапазоне значений перечисления (7.2). В противном случае результирующее значение не указано (и может не находиться в этом диапазоне).

поддержка не позволяя

int* ip = static_cast<int*>(&me);

и

5.2.9/11 prvalue типа "указатель на пары CV1 Б", где Б-класс типа, могут быть преобразованы в prvalue типа "указатель на cv2 Д", где Д-это класс, производный (п. 10) от B, если допустимое стандартное преобразование "указатель на D" в положение "указатель на B" существует (4.10), cv2 такое же резюме-квалификация, или больше резюме квалификация, чем, почтовый индекс CV1, и б не виртуальный базовый класс Д, ни базовый класс виртуальных базовый класс D. значение нулевого указателя (4.10) преобразуется в значение указателя null типа назначения. Если значение prvalue типа "указатель на cv1 B" указывает на B, который фактически является подобъектом объекта типа D, результирующий указатель указывает на заключающий объект типа D. В противном случае результат приведения не определен.

static_cast нельзя использовать для приведения &me до int* С MyEnum и int не связаны по наследству.


Я думаю, причина первая static_cast может работать с функциями и библиотеками, которые ожидают старого стиля enum или даже использовал кучу определенных значений для перечислений и непосредственно ожидал интегрального типа. Но нет никакой другой логической связи между типом enum и интегральный тип, поэтому вы должны использовать reinterpret_cast если вы хотите, чтобы бросить. но если у вас есть проблемы с reinterpret_cast вы можете использовать ваш собственный помощник:

template< class EnumT >
typename std::enable_if<
    std::is_enum<EnumT>::value,
    typename std::underlying_type<EnumT>::type*
>::type enum_as_pointer(EnumT& e)
{
    return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}

или

template< class IntT, class EnumT >
IntT* static_enum_cast(EnumT* e, 
    typename std::enable_if<
        std::is_enum<EnumT>::value &&
        std::is_convertible<
            typename std::underlying_type<EnumT>::type*,
            IntT*
        >::value
    >::type** = nullptr)
{
    return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}

при этом ответ может не удовлетворить вас о reason of prohibiting static_cast of enum pointers, это дает вам безопасный способ использования reinterpret_cast С ними.