Безопасно ли повторно интерпретировать переменную класса 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 "Преобразуется в тип" указатель на cv T2 ", Результат static_cast<cvT2*>(static_cast<cvvoid*>(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);
Другие вопросы по тегам