В 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"}
Я пытаюсь понять, почему это должно быть: есть ли что-то в механизме перечисления, которое затрудняет или бессмысленно поддерживать это? Это простое упущение в стандарте? Что-то другое?
Мне кажется, что если тип enum действительно построен поверх целочисленного типа, как указано выше, мы должны иметь возможность приводить не только значения, но и указатели. Мы все еще можем использовать reinterpret_cast<int*>
или бросок в стиле C, но это больший молот, чем я думал, что нам понадобится.
TL; DR: Дизайнеры 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 имеют одно и то же представление (стандарт даже назвал бы их «типами стандартной компоновки»), вы не можете конвертировать между ними без reinterpret_cast
, Точно так же это тоже ошибка:
class C
{
public:
int x;
};
void foo(C* c)
{
int* intPtr = static_cast<int*>(c); //error
}
Хотя мы знаем, что единственное, что есть в C, это int, и вы можете свободно получить к нему доступ, static_cast
выходит из строя. Зачем? Это явно не указано, что эти типы связаны. C ++ был разработан для поддержки объектно-ориентированного программирования, которое обеспечивает различие между композицией и наследованием. Вы можете конвертировать типы, связанные наследованием, но не типы, связанные композицией.
Основываясь на поведении, которое вы видели, ясно, что перечисления со строгой типизацией связаны композицией с их базовыми типами. Почему эта модель могла быть выбрана стандартным комитетом?
Есть много статей по этому вопросу, написанных лучше, чем что-либо, что я мог бы здесь разместить, но я попытаюсь подвести итог. Когда использовать композицию, а когда использовать наследование — это, безусловно, серая область, но в этом случае есть много точек в пользу композиции.
Вы могли бы несколько дней спорить о том, лучше ли наследование или композиция в этом случае, но в конечном итоге нужно было принять решение, и поведение было смоделировано на композиции.
Вместо этого посмотрите на это немного по-другому. Ты не можешь static_cast
long*
в int*
даже если int
а также long
имеют идентичные базовые представления. По той же причине перечисление, основанное на int
еще рассматривается как уникальный, не связанный тип int
и как таковой требует reinterpret_cast
,
Перечисление — это отдельный тип (3.9.2) с именованными константами. […] Каждое перечисление определяет тип, который отличается от всех других типов. […] Два типа перечисления совместимы с макетом, если имеют одинаковый базовый тип.
[Dcl.enum] (§ 7.2)
Базовый тип определяет расположение перечисления в памяти, а не его отношение к другим типам в системе типов (как говорит стандарт, это отличный тип, своего рода). Указатель на enum : int {}
никогда не может неявно преобразовать в int*
так же, как указатель на struct { int i; };
не могу, хотя все они выглядят одинаково в памяти.
Так почему неявное преобразование в int
работа в первую очередь?
Для перечисления, базовый тип которого является фиксированным, значения
перечисления являются значениями базового типа. […] Значение
перечислитель или объект типа перечисления с незаданной областью
преобразуется в целое число путем интегрального повышения (4.5).
[Dcl.enum] (§ 7.2)
Таким образом, мы можем присвоить значения перечисления int
потому что они имеют тип int
, Объект типа enum может быть назначен int
из-за правил целочисленного продвижения. Кстати, стандарт здесь особо указывает, что это справедливо только для перечислений в стиле C (с незаданной областью). Это означает, что вам все еще нужно 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 ответ: Статическое приведение указателя enum
на указатель на int
аналогично приведению указателя от одного целочисленного типа к другому — даже если оба имеют одинаковый макет памяти внизу, а значения одного неявно преобразуются в другой по правилам интегрального продвижения, они по-прежнему не связаны между собой, поэтому static_cast
не будет работать здесь.
Я думаю, ошибка в том, что
enum class MyEnum : int {};
это не совсем наследство. Конечно можно сказать MyEnum
является int
, Однако он отличается от классического наследования тем, что не все операции, доступные на int
s доступны для MyEnum
также.
Давайте сравним это со следующим: Круг — это эллипс. Однако почти всегда было бы неправильно внедрять CirlceShape
как наследование от EllipseShape
поскольку не все операции, которые возможны на эллипсах, также возможны для круга. Простым примером будет масштабирование фигуры в направлении х.
Следовательно, думать о перечислимых классах как наследуя от целочисленного типа приводит к путанице в вашем случае. Вы не можете увеличивать экземпляр класса enum, но можете увеличивать целые числа. Так как это не действительно наследование, имеет смысл запретить наведение указателей на эти типы статически. Следующая строка не является безопасной:
++*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 Значение целочисленного типа или типа перечисления может быть явно преобразовано в тип перечисления. Значение не изменяется, если исходное значение находится в диапазоне значений перечисления (7.2). В противном случае результирующее значение не определено (и может не входить в этот диапазон).
Поддержка не позволяет
int* ip = static_cast<int*>(&me);
можно получить из:
5.2.9 / 11 Значение типа «указатель на cv1 B», где B — тип класса, может быть преобразовано в значение типа «указатель на cv2 D», где D — класс, производный (раздел 10) от B , если существует допустимое стандартное преобразование из «указателя на D» в «указатель на B» (4.10), cv2 является той же квалификацией cv, что и cv1-квалификация или более высокой, чем cv1, а B не является ни виртуальным базовым классом D или базовый класс виртуального базового класса D. Значение нулевого указателя (4.10)
преобразуется в значение нулевого указателя типа назначения. Если значение типа «указатель на 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
с ними.