Подтвердить что ты не робот

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

В С++ 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"
}

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

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

4b9b3361

Ответ 1

TL; DR: Дизайнеры С++ не любят печатать пинком.

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

Безопасность типа С++

В С++ в целом типы не связаны между собой, если явно не указано, что они связаны (через наследование). Рассмотрим этот пример:

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 имеют то же самое представление (стандарт даже назвал бы их стандартными типами макетов), вы не можете конвертировать между ними без reinterpret_cast. Аналогично, это также ошибка:

class C
{
public:
    int x;
};

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

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

Основываясь на поведении, которое вы видели, он четко определяет типизированные перечисления, связанные составом с их базовыми типами. Почему это может быть модель, выбранная стандартным комитетом?

Состав и наследование

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

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

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

Ответ 2

Вместо этого взгляните на это несколько иначе. Вы не можете static_cast a long* до int*, даже если int и long имеют одинаковые базовые представления. По той же причине перечисление, основанное на int, пока рассматривается как уникальный, несвязанный тип с int и как таковой требует reinterpret_cast.

Ответ 3

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

[dcl.enum] (§7.2)

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

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

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

[dcl.enum] (§7.2)

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

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

Последний фрагмент головоломки можно найти в [expr.static.cast] (§5.2.9), в котором описывается, как работает static_cast.

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

Это объясняет, почему работает ваш приказ от enum class до int.

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

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

Ответ 4

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

enum class MyEnum : int {};

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

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

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

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

Это может быть причиной того, что комитет запретил static_cast в этом случае. Обычно reinterpret_cast считается злым, а static_cast считается нормально.

Ответ 5

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

Поддержка разрешения

int iv = static_cast<int>(me);

можно получить из:

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

Поддержка разрешения

me = static_cast<MyEnum>(100);

можно получить из:

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

Поддержка запрета

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

можно получить из:

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

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

Ответ 6

Я думаю, что причина для первого 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 с ними.