Защитный код после переключения на перечисление никогда не достигается
Я просто попал в запутанную проблему при попытке скомпилировать некоторый код с помощью g++ 4.4.3.
Код ниже компилируется отлично, но вместо того, чтобы нажимать ожидаемое утверждение, когда я передаю "недопустимое" значение enum, функция просто возвращает 1. То, что я нахожу еще более странным, - это то, что когда я раскомментирую строки, относящиеся к значению перечисления E3, все начинает работать, как ожидалось.
Тот факт, что в блоке коммутатора нет записи по умолчанию, по дизайну. Мы компилируем с параметром -Wall для получения предупреждений о необработанных значениях перечисления.
enum MyEnum
{
E1,
E2,
//E3
};
int doSomethingWithEnum(MyEnum myEnum)
{
switch (myEnum)
{
case E1: return 1;
case E2: return 2;
//case E3: return 3;
}
assert(!"Should never get here");
return -1;
}
int main(int argc, char **argv)
{
// Should trigger assert, but actually returns 1
int retVal = doSomethingWithEnum(static_cast<MyEnum>(4));
std::cout << "RetVal=" << retVal << std::endl;
return 0;
}
Ответы
Ответ 1
Ваш оператор switch будет скомпилирован в это (g++ 4.4.5):
cmpl $1, %eax
je .L3
movl $1, %eax
jmp .L4
.L3:
movl $2, %eax
.L4:
leave
ret
Как можно видеть, утверждение полностью оптимизировано, и компилятор выбирает сравнение с E2 и возвращает 1 во всех остальных случаях. С тремя значениями перечисления он не может этого сделать.
В разделе 5.2.9 стандарта С++ 98 (статический литье) приводятся причины этого:
Значение интегрального или перечисляемого типа может быть явно преобразовано в тип перечисления. Значение не изменяется если исходное значение находится в пределах значений перечисления (7.2). В противном случае результирующее значение перечисления unspeci фи-е изд.
Другими словами, компилятор может использовать любое требуемое значение перечисления (в этом случае E1), если вы попытаетесь использовать недопустимое значение. Это включает в себя выполнение интуитивно понятной вещи и использование предоставленной незаконной стоимости или использование разных значений в зависимости от обстоятельств, поэтому поведение изменяется в зависимости от количества значений перечисления.
Ответ 2
Это не похоже на ошибку компилятора. Это больше похоже на поведение undefined.
В правиле говорится, что вы можете присвоить значение undefined Enum IF, это значение находится в диапазоне перечисления.
В вашем случае компилятору нужен только один бит для представления перечисления, поэтому он, вероятно, делает какую-то оптимизацию.
Ответ 3
Это совершенно нормально, ваш код зависит от неуказанного поведения.
В С++ перечисление должно содержать значения между его более низкими и более высокими значениями. Все, что еще может быть преобразовано или проигнорировано компилятором.
Например, если у меня есть enum Foo { min = 10, max = 11 };
, тогда компилятор может представить его одним значимым битом и добавить 10, когда я попрошу его распечатать или преобразовать в целое число.
В общем случае компиляторы не используют преимущества этого "уплотнения" из-за стоимости исполнения, они просто выбирают базовый тип, который может содержать все значения и 0
. Чаще всего это int
, если int
слишком мало.
Однако это не мешает им предполагать, что значения, содержащиеся в этом int
, ограничены определенным вами диапазоном. В вашем случае: [0, 2)
.
Таким образом, оператор switch
можно просто оптимизировать в сравнении с 0
, так как вы указали, что ваше перечисление может быть только 0
или 1
.
В том же духе if (foo == 3)
можно считать тавтологическим сравнением (всегда ложным) и оптимизированным.
Фактически, как описано в gcc-ошибке, сигнализируемой Хансом Пассатом, эта оптимизация обычно происходит при мощности границ 2s для gcc. Например, если у вас есть enum { zero, one, two, three };
и назначено 4
или выше, такое же поведение произойдет.
Обратите внимание, что фактически не сохраненное значение не влияет! Вот почему выражение печати работает так, как вы ожидали, и является источником путаницы.
Я не знаю, есть ли предупреждение, указывающее, что сохранение 4 в этом перечислении приведет к неуказанному поведению. В любом случае, он будет работать только для значений времени компиляции...
Hans Passat предоставил gcc "bug" , который открыт для отслеживания этой "проблемы".
И Эмиль Штирке дал оправдание (вот обновленная цитата для скопированных перечислений):
5.2.9/10
Значение интегрального или перечисляемого типа может быть явно преобразовано в тип перечисления. Значение не изменяется, если исходное значение находится в диапазоне значений перечисления (7.2). В противном случае результирующее значение не указано (и может быть не в этом диапазоне). Значение типа с плавающей запятой также может быть преобразовано в тип перечисления. Полученное значение совпадает с преобразованием исходного значения в базовый тип перечисления (4.9), а затем в тип перечисления.