Какие значения перечисления являются неопределенным поведением в C ++ 14 и почему? - PullRequest
0 голосов
/ 26 января 2019

Сноска в стандарте подразумевает, что любое значение выражения перечисления является определенным поведением;почему неопределенное поведение Sananizer Clang помечает значения вне допустимого диапазона?

Рассмотрим следующую программу:

enum A {B = 3, C = 7};

int main() {
  A d = static_cast<A>(8);
  return d + B;
}

Вывод в неопределенное поведение Sanitizer is:

$ clang++-5.0 -fsanitize=undefined -ggdb3 enum.cc && ./a.out 
enum.cc:5:10: runtime error: load of value 8, which is not a valid value for type 'A'

Обратите внимание, что ошибка не в static_cast, а в добавлении.Это также верно, если A создан (но не инициализирован), а затем int со значением 8 memcpy введен в A - ошибка убсана при добавлении, а не при начальной загрузке.

IIUC, ubsan в новых клангах действительно выдает ошибку на static_cast в режиме C ++ 17.Я не знаю, если этот режим также находит ошибку в memcpy.В любом случае этот вопрос сфокусирован на C ++ 14.

Указанная ошибка соответствует следующим частям стандарта:

dcl.enum :

Для перечисления, базовый тип которого является фиксированным, значения перечисления являются значениями базового типа.В противном случае значения перечисления являются значениями, представленными гипотетическими целочисленными типами с минимальным показателем диапазона M, так что все перечислители могут быть представлены.Ширина наименьшего битового поля, достаточно большая, чтобы вместить все значения типа перечисления, равна M. Можно определить перечисление, значения которого не определены ни одним из его перечислителей.Если список перечислителя пуст, значения перечисления такие, как если бы перечисление имело единственный перечислитель со значением 0. 100

Значения перечисления A имеют значения от 0 до 7 включительно, а «показатель диапазона» M равен 3. Оценка выражения типа A со значением 8 является неопределенным поведением в соответствии с expr.pre :

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

Но есть один сбой: что сноска из dcl.enum читает:

Этот набор значений используется для определения семантики продвижения и преобразования для типа перечисления. Это не препятствует тому, чтобы выражение типа перечисления имело значение, выходящее за пределы этого диапазона. [выделение шахты]

Вопрос: Почему выражение со значением 8 и типом A неопределенное поведение, если «[dcl.enum] не исключает выражения типа перечисления со значением, выходящим за пределы этого диапазона»?

Ответы [ 3 ]

0 голосов
/ 29 января 2019

Обратите внимание на формулировку сноски 100: "[Этот набор значений] не исключает [материал]." Это не означает, что "материал" является действительным;это просто подчеркивает, что этот раздел не объявляет материал недействительным.Это на самом деле нейтральное утверждение, которое должно напомнить о ошибке исключенного среднего .Что касается этого раздела, значения за пределами значений перечисления не одобрены и не отклонены.Этот раздел определяет, какие значения находятся за пределами значений перечисления, но он оставлен на усмотрение других разделов (например, expr.pre), чтобы решить вопрос о допустимости использования таких значений.

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


Чтобы лучше понять, на что именно жалуется clang, попробуйте следующий код:

enum A {B = 3, C = 7};

int main() {
  // Set a variable of type A to a value outside A's set of values.
  A d = static_cast<A>(8);

  // Try to evaluate an expression of type A with this too-big value.
  if ( !static_cast<bool>(static_cast<A>(8)) )
    return 2;

  // Try again, but this time load the value from d.
  if ( !static_cast<bool>(d) ) // Sanitizer flags only this
    return 1;

  return 0;
}

Дезинфицирующее средство не жалуется на принудительное использование значения 8 в переменной типа A.Он не жалуется на оценку выражения типа A, которое имеет значение 8 (первое if).Тем не менее, он жалуется, когда значение 8 исходит из (* загружено из) переменной типа A.

0 голосов
/ 31 января 2019

Я не очень знаком с компилятором Clang, так как я привык к Visual Studio.В настоящее время я использую Visual Studio 2017. Мне удалось скомпилировать и запустить ваш код с языковым флагом, установленным как для c ++ 14, так и для c ++ 17 в сборках отладки x86 и x64.Вместо того, чтобы возвращать добавление в вашем примере:

return d + B;

Я решил вывести их на консоль:

std::cout << (d + B);

и во всех 4 случаях мой компиляторвывел значение 11.

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

Я перешел по вашей ссылке и прочитал раздел 8, на который вы ссылались, но то, что привлекло мое внимание из этого черновика, это подробности из других разделов, а именно 7 и 10.


Раздел 7 сообщает:

Для перечисления, базовый тип которого не фиксирован, базовый тип является целочисленным типом, который можетпредставляют все значения перечислителя, определенные в перечислении.Если ни один целочисленный тип не может представлять все значения перечислителя, перечисление неверно сформировано.Определяется реализацией, какой интегральный тип используется в качестве базового типа, за исключением того, что базовый тип не должен быть больше, чем int, если только значение перечислителя не может поместиться в int или unsigned int.Если список-перечислитель пуст, базовый тип выглядит так, как если бы перечисление имело единственный перечислитель со значением 0.

Но именно это предложение или предложение привлекли мое внимание:

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


Раздел 10 сообщает:

Значение перечислителя или объекта типа перечисления с незаданной областью преобразуется в целое числоинтегральное продвижение.[Пример:

enum color { red, yellow, green=20, blue };
color col = red;
color* cp = &col;
if (*cp == blue)     // ...

делает цвет типом, описывающим различные цвета, а затем объявляет col как объект этого типа и cp как указатель на объект этого типа.Возможные значения объекта типа color: красный, желтый, зеленый, синий;эти значения могут быть преобразованы в целочисленные значения 0, 1, 20 и 21. Поскольку перечисления являются разными типами, объектам типа color могут быть назначены только значения типа color.

color c = 1;        // error: type mismatch, no conversion from int to color
int i = yellow;     // OK: yellow converted to integral value 1, integral promotion

Обратите внимание, что это неявное преобразование перечисления в int не предусмотрено для перечисления с областью действия:

enum class Col { red, yellow, green };
int x = Col::red;   // error: no Col to int conversion
Col y = Col::red;
if (y) { }          // error: no Col to bool conversion

- конец примера]

Именно этидве строки, которые привлекли мое внимание:

color c = 1;        // error: type mismatch, no conversion from int to color
int i = yellow;     // OK: yellow converted to integral value 1, integral promotion

Итак, давайте вернемся к вашему примеру:

enum A {B = 3, C = 7};

int main() {
  A d = static_cast<A>(8);
  return d + B;
}

Здесь A - полный тип, B & C - это перечислители, которые оцениваются как постоянное выражение целочисленного типа путем повышения и устанавливаются в значения 3 и 7 соответственно.Это касается объявления enum A{...};

Внутри main() вы теперь объявляете экземпляр или объект A с именем d, поскольку A является полным типом.Затем вы присваиваете d значение 8, которое является константным выражением или константным литералом через механизм static_cast.Я не уверен на 100%, выполняет ли каждый компилятор static_cast одинаково точно или нет;Я не уверен, зависит ли это от компилятора.

Итак, d - это объект типа A, но поскольку значение 8 отсутствует в списке перечислений, я считаю, что оно подпадает под условие implementation defined.Это должно затем привести d к целочисленному типу.

Затем в вашем последнем утверждении, в котором вы возвращаете d+B.

Leскажем, что d был преобразован в целочисленный тип со значением 8, затем вы добавляете перечисляемое значение B, которое составляет 3 к 8, и, следовательно, вы должны получить вывод 11 в котором у меня есть все 4 моих тестовых случая на visual studio.

Теперь, что касается вашего компилятора с Clang, я не могу сказать, но, насколько я могу судить, это, похоже, не вызывает каких-либо ошибок.или неопределенное поведение по крайней мере в соответствии с Visual Studio.Опять же, поскольку этот код, по-видимому, определяется реализацией, я думаю, что он сильно зависит от вашего конкретного компилятора и его версии, а также от языковой версии, под которой вы его компилируете.

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


-Edit-

Я решил запустить это через мой отладчик, и я поставил точку останова в этой строке:

A d = static_cast<A>(8);

ЗатемЯ прошел через выполнение этой строки кода и посмотрел на значение в отладчике.Здесь, в Visual Studio, d имеет значение 8.Однако под его типом он указан как A, а не int.Так что я не знаю, продвигает ли это его до int или нет, или это может произойти при оптимизации компилятора, что-то скрытое, например asm, которое рассматривает d как int илиunsigned int и т.д .;но Visual Studio позволяет мне присваивать целочисленное значение через static_cast перечисляемому типу.Однако, если я удаляю static_cast, он не скомпилируется, заявив, что вы не можете присвоить тип int типу A.

Это заставляет меня поверить, что мое первоначальное утверждение выше действительно неверноили только частично правильно.Компилятор не полностью «переводит» его в целочисленный тип при присваивании, так как d по-прежнему остается экземпляром A, если только он не делает это под капотом, о котором я не знаю.

Я еще не проверил, чтобы увидеть asm этого кода, чтобы увидеть, какие инструкции по сборке генерируются Visual Studio ... поэтому в настоящее время я не могу провести полную оценку на данный момент.Теперь, позже, если у меня будет больше свободного времени;Я могу заглянуть в него, чтобы увидеть, какие строки asm генерирует мой компилятор, чтобы увидеть основные действия, выполняемые компилятором.

0 голосов
/ 26 января 2019

Clang отмечает использование static_cast для значения, которое находится вне диапазона. Поведение не определено, если целое значение находится вне диапазона перечисления.

Стандарт C ++ 5.2.9 Статическое приведение [expr.static.cast] пункт 7

Значение целочисленного типа или типа перечисления может быть явно преобразовано в тип перечисления. Значение не изменяется, если исходное значение в пределах значений перечисления (7.2). В противном случае результирующее значение перечисления не указано / не определено (начиная с C ++ 17).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...