Можно ли reinterpret_cast перечислить переменную класса enum в ссылку базового типа?

Я видел reinterpret_cast, используемый для применения инкремента к классам перечисления, и я хотел бы знать, допустимо ли это использование в стандартном С++.

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))
{
    ...
}

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

Ответ 1

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

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) и стандартное преобразование функции-to-pointer (4.3) в выражении v. Ниже перечислены преобразования, которые могут быть выполнены явно с использованием reinterpret_cast. Никакое другое преобразование не может быть выполнено явно с помощью reinterpret_cast.

(акцент мой)

Реинтерпретация с использованием ссылок основана на указателях в соответствии с 5.2.10/11:

11 Выражение glval типа T1 может быть передано типу "ссылка на T2", если выражение типа "указатель на T1" может быть явно преобразовано в введите "указатель на T2" с помощью reinterpret_cast. Результат относится к тому же объекту, что и исходное значение glvalue, но с указанным типом. [Примечание. То есть для lvalues ​​ссылочный листинг reinterpret_cast<T&>(x) имеет тот же эффект, что и преобразование *reinterpret_cast<T*>(&x) со встроенными операторами & и * (и аналогично для reinterpret_cast<T&&>(x)). - end note] Временное создание не производится, копия не производится, а конструкторы (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< cv T2*>(static_cast< cv void*>(v)), если оба T1 и T2 являются стандартными типами макетов (3.9), а требования к выравниванию T2 не более строгие, чем теги T1, или если один из типов - void. Преобразование prvalue типа "указатель на 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 должен быть идентичным (исключая cv)
  • 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 несут ответственность за внесение кода, который я дал в первоначальном ответе и который вы все еще можете найти наверху.

Ответ 2

Приращение обращается к значению foo через значение l другого типа, которое является undefined, за исключением случаев, перечисленных в 3.10 [basic.lval]. Типы перечислений и их базовые типы не входят в этот список, поэтому код имеет поведение undefined.

С некоторыми компиляторами, которые поддерживают нестандартные расширения, вы можете сделать это с помощью функции push-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, потому что может быть несколько счетчиков с одним и тем же значением или значения типа перечисления, которые не имеют соответствующей метки счетчика.)

Ответ 3

Это безопасно, если оно относится к точному типу перечисления.

Если базовый тип класса перечисления изменяет, что ++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);