Безопасно ли повторно интерпретировать переменную класса enum для ссылки на базовый тип?
Я видел reinterpret_cast
Я использовал для приращения классов enum, и я хотел бы знать, является ли это использование приемлемым в стандарте C++.
enum class Foo : int8_t
{
Bar1,
Bar2,
Bar3,
Bar4,
First = Bar1,
Last = Bar4
};
for (Foo foo = Foo::First; foo <= Foo::Last; ++reinterpret_cast<int8_t &>(foo))
{
...
}
Я знаю, что приведение к ссылке на базовый класс безопасно в случае тривиальных классов. Но поскольку enum-классы не являются событиями, неявно преобразуемыми в их базовые типы, я не уверен, будет ли гарантированно работать вышеуказанный код во всех компиляторах. Есть какие-нибудь подсказки?
3 ответа
Возможно, вы захотите перегрузить оператор ++
для вашего перечисления, если вы действительно хотите повторить его значения:
Foo& operator++( Foo& f )
{
using UT = std::underlying_type< Foo >::type;
f = static_cast< Foo >( static_cast< UT >( f ) + 1 );
return f;
}
и использовать
for (Foo foo = Foo::First; foo != Foo::Last; ++foo)
{
...
}
Чтобы ответить на вопрос о том, reinterpret_cast
разрешено, все начинается с 5.2.10 / 1:
5.2.10 Переинтерпретация приведения [expr.reinterpret.cast]
1 Результат выражения
reinterpret_cast<T>(v)
является результатом преобразования выраженияv
печататьT
, ЕслиT
тип ссылки lvalue или ссылка rvalue на тип функции, результат - значение lvalue; еслиT
является rvalue ссылкой на тип объекта, результатом является xvalue; в противном случае результатом является prvalue, и в выражении выполняются стандартные преобразования lvalue-to-rvalue (4.1), array-to-pointer (4.2) и function-to-pointer (4.3)v
, Преобразования, которые могут быть выполнены явно с использованиемreinterpret_cast
перечислены ниже. Никакое другое преобразование не может быть выполнено явно, используяreinterpret_cast
,
(акцент мой)
Реинтерпретация с использованием ссылок основана на указателях в соответствии с 5.2.10 / 11:
11 glvalue выражение типа
T1
может быть приведен к типу "ссылка наT2
Если выражение типа "указатель наT1
"Можно явно преобразовать в тип" указатель наT2
" используяreinterpret_cast
, Результат ссылается на тот же объект, что и источник glvalue, но с указанным типом. [ Примечание: то есть для lvalues, приведениеreinterpret_cast<T&>(x)
имеет тот же эффект, что и преобразование*reinterpret_cast<T*>(&x)
со встроенным&
а также*
операторы (и аналогично дляreinterpret_cast<T&&>(x)
). - примечание конца ] Временное создание не производится, копирование не производится, а конструкторы (12.1) или функции преобразования (12.3) не вызываются.
Что превращает вопрос из этого:
reinterpret_cast<int8_t&>(foo)
Насколько это законно:
*reinterpret_cast<int8_t*>(&foo)
Следующая остановка - 5.2.10 / 7:
7 Указатель объекта может быть явно преобразован в указатель объекта другого типа. Когда prvalue
v
типа "указатель наT1
"Преобразуется в тип" указатель на cvT2
", Результатstatic_cast<
cv
T2*>(static_cast<
cv
void*>(v))
если обаT1
а такжеT2
стандартные типы (3.9) и требования к выравниваниюT2
не более строгие, чем те,T1
или если какой-либо типvoid
, Преобразование значения типа "указатель наT1
Указатель на типT2
" (гдеT1
а такжеT2
являются типами объектов и где требования выравниванияT2
не более строгие, чем те,T1
) и обратно к исходному типу возвращает исходное значение указателя. Результат любого другого такого преобразования указателя не определен.
Учитывая 3.9/9 оба int8_t
и ваш тип перечисления являются стандартными типами разметки, в который вопрос теперь преобразован:
*static_cast<int8_t*>(static_cast<void*>(&foo))
Это где вам не повезло. static_cast
определено в 5.2.9, и нет ничего, что делает вышеупомянутое законным - фактически 5.2.9/5 является явным намеком на то, что это незаконно. Другие пункты не помогают:
- 5.2.9 / 13 требует
T*
->void*
->T*
гдеT
должен быть идентичным (без учета резюме) - 5.2.9/9 и 5.2.9/10 не об указателях, а о значениях
- 5.2.9/11 о классах и иерархиях классов
- 5.2.9/12 о указателях членов класса
Мой вывод из этого заключается в том, что ваш код
reinterpret_cast<int8_t&>(foo)
не является законным, его поведение не определяется стандартом.
Также обратите внимание, что вышеупомянутые 5.2.9/9 и 5.2.9/10 несут ответственность за то, чтобы сделать код законным, который я дал в первоначальном ответе и который вы все еще можете найти в верхней части.
Приращение получает доступ к значению foo
через lvalue другого типа, что является неопределенным поведением, за исключением случаев, перечисленных в 3.10 [basic.lval]. Типы перечисления и их базовые типы не включены в этот список, поэтому код имеет неопределенное поведение.
С некоторыми компиляторами, которые поддерживают нестандартные расширения, вы можете сделать это через type-punning:
union intenum
{
int8_t i;
Foo e;
};
intenum ie;
for (ie.e = Foo::First; ie.e <= Foo::Last; ++ie.i)
// ...
но это тоже не переносимо, потому что доступ intenum::i
после сохранения значения в intenum::e
не допускается стандартом.
Но почему бы просто не использовать целое число и конвертировать по мере необходимости?
for (int8_t i = static_cast<int8_t>(Foo::First);
i <= static_cast<int8_t>(Foo::Last);
++i)
{
Foo e = static_cast<Foo>(i);
// ...
}
Это портативный и безопасный.
(Это все еще не очень хорошая идея, IMHO, потому что может быть несколько перечислителей с одинаковым значением или значений типа перечисления, которые не имеют соответствующей метки перечислителя.)
Это безопасно, пока оно приводит к точному базовому типу перечисления.
Если базовый тип класса enum изменяется, ++reinterpret_cast<int8_t &>(foo)
молча ломается
Более безопасная версия:
foo = static_cast<Foo>(static_cast<std::underlying_type<Foo>::type>(foo) + 1);
Или же,
++reinterpret_cast<std::underlying_type<Foo>::type&>(foo);