Если у меня есть оператор 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
со значением, которое не учитывается.
@AviBerger Это не ограничивается типами с плавающей запятой. eel.is/c++draft/dcl.enum#8
@HolyBlackCat Спасибо. Стандарт является лучшим источником, чем cppreference, и формулировка вашей цитаты отличается и яснее, чем полная цитата из cppreference.
Да, я думаю, в теории они могли бы. Стандартного способа добраться до default
не существует.
Как упоминалось на странице cppreference, стандарт по существу говорит в [dcl.enum]/8, что возможные значения перечисления без фиксированного базового типа от нуля до, исключительная, наименьшая степень двойки, способная соответствовать всем объявленные значения перечислителя. Приведение от базового типа к значениям за пределами этого диапазона через static_cast
является явно неопределенным поведением.
Может быть, есть место для споров о memcpy
/std::bit_cast
от базового типа к перечислению, но я не думаю, что это сработает. Связанный стандартный проход, по-видимому, определяет исключительно значения перечисления, поэтому представления объектов не могут представлять какие-либо дополнительные значения (при условии, что они с самого начала являются допустимыми представлениями объектов перечисления).
В настоящее время стандарт даже не гарантирует того же представления, что и базовый тип, для значений, которые имеет перечисление (хотя это может быть дефектом).
Однако это было бы несовместимо с C, где разрешены все значения базового типа. Поскольку наименьший возможный базовый тип — это char
, который имеет ширину не менее 8 бит, всегда будет возможно достичь default
в C.
Поэтому я не ожидаю, что какой-либо компилятор сочтет default
недостижимым без дополнительных знаний о диапазоне аргумента.
Перечисления с фиксированным базовым типом (который включает в себя все enum class
) задаются по-разному и гарантированно поддерживают все значения базового типа, как в случае C для всех перечислений.
Таким образом, перечисление с 63 различными вариантами, скорее всего, не может игнорировать регистр по умолчанию, а перечисление с 64 различными вариантами — может. В таком случае я бы сказал, что оптимизация бессмысленна и опасна.
@ gnasher729 Обычно его можно использовать для таблиц прыжков. Не предполагая, что значения за пределами диапазона невозможны, компилятор должен добавить проверку, находится ли значение за пределами диапазона таблицы переходов. Если бы использовался ограниченный диапазон, компилятор мог бы просто создать таблицу переходов для этого размера (наибольшее значение перечислителя, округленное до степени двойки) и проигнорировать возможность значения, лежащего над ним, избавившись от дополнительного условного выражения.
@user17732522 user17732522 Это хорошее замечание о таблицах переходов. Мне кажется, что это был бы более коварный случай, когда оператор по умолчанию определен для некоторых значений, не относящихся к перечислению, но не для других. Хотя, как вы говорите, использование класса перечисления или перечисления фиксированного типа позволит избежать всех проблем.
Некоторые компиляторы очень агрессивно предполагают, что неопределенное поведение не происходит (и, следовательно, оптимизируют код). Но в C слишком просто создать значение перечисления, которое не соответствует ни одному регистру. И код, который вы показали, был явно предназначен для обнаружения неправильных значений.
Я никогда не видел и не слышал, чтобы компилятор оптимизировал этот случай. Отличие в таком языке, как Swift, где установить для перечисления значение, которое не упоминается в качестве регистра, очень и очень сложно сделать.
Что-то связанное, вы можете использовать std::unreachable(C++23) и другие специальные функции компилятора, такие как
__builtin_unreachable
, чтобы стимулировать оптимизацию, даже если вы не указали все случаи.