Можно ли оптимизировать оператор switch по умолчанию для перечисления

Если у меня есть оператор switch, который явно обрабатывает все случаи перечисления, может ли компилятор оптимизировать оператор case по умолчанию?

enum MyEnum {
 ZERO = 0,
 ONE = 1,
 TWO = 2,
 THREE = 3,
};

bool foo(MyEnum e) {
 switch(e) {
 case ZERO:
 case ONE:
 case TWO:
 case THREE:
  return true;
 default:    // Could a compiler optimise this away?
  return false;
 }
}

Cpp Reference говорит о перечислениях (выделено мной):

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

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

Это указывает на то, что в приведенном выше примере будет разрешено оптимизировать оператор по умолчанию, поскольку базовый тип не является фиксированным и указано каждое 2-битное значение (хотя, возможно, вам потребуется включить отрицательные значения до -4).

Однако неясно, относится ли это также к перечислениям фиксированного типа или к классам перечислений.


На практике GCC, clang и MSVC не предполагают, что перечисление является одним из определенных значений. (Godbolt с включенной полной оптимизацией, например -O3 -std=c++20.)

Это пропущенная оптимизация или стандарт говорит, что если реализация выбирает int в качестве базового типа (даже если вы не указали), то любое значение int допустимо?

CppReference показывает пример под цитируемым абзацем:

enum access_t { read = 1, write = 2, exec = 4 }; 
 // enumerators: 1, 2, 4 range: 0..7
access_t y = static_cast<access_t>(8);   // undefined behavior since CWG1766

Это дает понять, что «диапазон» привязан к именованным значениям перечислителя, а не к полной ширине любого целочисленного типа. Предполагая, что пример cppreference является точным отражением стандарта ISO, конечно, это подразумевает, что явное или неявное преобразование не может легально создавать объект enum со значением, которое не учитывается.

Что-то связанное, вы можете использовать std::unreachable(C++23) и другие специальные функции компилятора, такие как __builtin_unreachable, чтобы стимулировать оптимизацию, даже если вы не указали все случаи.

Ranoiaetep 14.11.2022 06:51

@AviBerger Это не ограничивается типами с плавающей запятой. eel.is/c++draft/dcl.enum#8

HolyBlackCat 14.11.2022 19:21

@HolyBlackCat Спасибо. Стандарт является лучшим источником, чем cppreference, и формулировка вашей цитаты отличается и яснее, чем полная цитата из cppreference.

Avi Berger 14.11.2022 19:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
3
227
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Да, я думаю, в теории они могли бы. Стандартного способа добраться до default не существует.

Как упоминалось на странице cppreference, стандарт по существу говорит в [dcl.enum]/8, что возможные значения перечисления без фиксированного базового типа от нуля до, исключительная, наименьшая степень двойки, способная соответствовать всем объявленные значения перечислителя. Приведение от базового типа к значениям за пределами этого диапазона через static_cast является явно неопределенным поведением.

Может быть, есть место для споров о memcpy/std::bit_cast от базового типа к перечислению, но я не думаю, что это сработает. Связанный стандартный проход, по-видимому, определяет исключительно значения перечисления, поэтому представления объектов не могут представлять какие-либо дополнительные значения (при условии, что они с самого начала являются допустимыми представлениями объектов перечисления).

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

Однако это было бы несовместимо с C, где разрешены все значения базового типа. Поскольку наименьший возможный базовый тип — это char, который имеет ширину не менее 8 бит, всегда будет возможно достичь default в C.

Поэтому я не ожидаю, что какой-либо компилятор сочтет default недостижимым без дополнительных знаний о диапазоне аргумента.

Перечисления с фиксированным базовым типом (который включает в себя все enum class) задаются по-разному и гарантированно поддерживают все значения базового типа, как в случае C для всех перечислений.

Таким образом, перечисление с 63 различными вариантами, скорее всего, не может игнорировать регистр по умолчанию, а перечисление с 64 различными вариантами — может. В таком случае я бы сказал, что оптимизация бессмысленна и опасна.

gnasher729 15.11.2022 20:30

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

user17732522 15.11.2022 21:39

@user17732522 user17732522 Это хорошее замечание о таблицах переходов. Мне кажется, что это был бы более коварный случай, когда оператор по умолчанию определен для некоторых значений, не относящихся к перечислению, но не для других. Хотя, как вы говорите, использование класса перечисления или перечисления фиксированного типа позволит избежать всех проблем.

dast99 16.11.2022 04:53

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

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

Другие вопросы по теме