Какие значения перечисления являются неопределенным поведением в С++ 14 и почему?

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

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

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

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

Вывод под дезинфицирующее средство неопределенного поведения:

$ 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 — ошибка ubsan возникает при добавлении, а не при начальной загрузке.

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

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

dcl.enum:

For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type. Otherwise, the values of the enumeration are the values representable by a hypothetical integer types with minimal range exponent M such that all enumerators can be represented. The width of the smallest bit-field large enough to hold all the values of the enumeration type is M. It is possible to define an enumeration that has values not defined by any of its enumerators. If the enumerator-list is empty, the values of the enumeration are as if the enumeration had a single enumerator with value 0.100

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

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.

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

This set of values is used to define promotion and conversion semantics for the enumeration type. It does not preclude an expression of enumeration type from having a value that falls outside this range. [emphasis mine]

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

Он помечает static_cast.

KamilCuk 26.01.2019 17:15

Это не исключает наличия значения, выходящего за пределы диапазона, но и не требует этого.

1201ProgramAlarm 26.01.2019 17:19

Возможный дубликат Может ли static_cast генерировать исключение в C++?

KamilCuk 26.01.2019 17:39

Обратите внимание, что ошибка не на static_cast, а на дополнении. Это также верно, если A создается (но не инициализируется), а затем int со значением 8 привязывается к memcpy — ошибка ubsan возникает при добавлении, а не при начальной загрузке. IIUC, ubsan отмечает ошибку на A в режиме C++17. Я не знаю, находит ли этот режим ошибку в static_cast.

jbapple 26.01.2019 19:43

@jbapple: примечания не являются нормативными. Таким образом, вы не можете полагаться на них для четко определенного поведения.

P.W 29.01.2019 08:38

@P.W: Многие примечания предназначены для того, чтобы указать, как ведут себя реализации качестводолжен, если они не документируют веская причина поступить иначе. Поскольку Стандарт не делает попытки предписать, чтобы любая реализация подходила для какой-либо конкретной цели, почти все программы должны полагаться на реализацию, имеющую качество, соответствующее выполняемым задачам. За исключением случаев, когда нужно обойти ограничения низкокачественной реализации, следует иметь возможность полагаться на качественные реализации, ведущие себя так, как описано в большинстве примечаний, если они не документируют противоположное поведение.

supercat 30.01.2019 19:23

@supercat: Спасибо за комментарий. Возьмите этот конкретный пример. Clang выдает ошибку во время выполнения, а GCC — нет. Я просмотрел руководство GCC, и, насколько я мог видеть, они не записывают никаких причин, по которым этого не делают. Итак, можем ли мы сделать вывод, что GCC не является качественной реализацией? Примет ли GCC отчет об ошибке на этом основании? Думаю, нет.

P.W 31.01.2019 08:02

Сложно сказать; Я не совсем знаком с Clang... Я использую Visual Studio 2017, и я установил свой язык как на С++ 14, так и на 17, и я скомпилировал и запустил ваш код как в x86, так и в x64, но вместо возврата 0 , я распечатал дополнение в консоли, и все пробные версии напечатали 11. Похоже, это вообще не проблема с Visual Studio. Я тоже не знаю о GCC, потому что не пробовал. Это может быть проблема, зависящая от компилятора.

Francis Cugler 31.01.2019 17:56

@P.W: В местах, где Стандарт не налагает требований, но большинство реализаций, предназначенных для определенной цели, ведут себя согласованным образом, любая качественная реализация, предназначенная для этой цели, ведет себя иначе должен документировать вескую причину для этого. Из того, что я могу сказать, большинство реализаций выбирают для каждого перечисления целочисленный тип, который достаточно велик, чтобы содержать все его значения, хранить перечисление с использованием этого типа и разрешать любое значение, которое соответствует этому типу, для хранения в перечислении. Есть случаи, когда другое поведение могло бы быть более полезным, и...

supercat 31.01.2019 20:15

... не было бы ничего «плохого» в реализации, которая ведет себя каким-то другим образом, который лучше удовлетворял бы потребности своих клиентов, при условии, что он документирует поведение, и при условии, что альтернативное поведение действительно служит потребностям его клиентов.

supercat 31.01.2019 20:18
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
10
2 163
3

Ответы 3

Clang помечает использование static_cast для значения, выходящего за пределы допустимого диапазона. Поведение не определено, если значение интеграла не входит в диапазон перечисления.

C++ standard 5.2.9 Static cast [expr.static.cast] paragraph 7

A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the resulting enumeration value is unspecified / undefined (since C++17).

Обратите внимание, что ошибка не на static_cast, а на дополнении. Это также верно, если A создается (но не инициализируется), а затем int со значением 8 привязывается к memcpy — ошибка ubsan возникает при добавлении, а не при начальной загрузке. IIUC, ubsan отмечает ошибку на A в режиме C++17. Я не знаю, находит ли этот режим ошибку в static_cast.

jbapple 26.01.2019 19:43

Что означает value is within the range of the enumeration values? Действительно ли 3 для enum{A,B,C}? Тип должен содержать 2 ^ 2 значения, так что я думаю, да. Как насчет 256 для enum class: uint16_t{A,B,C}? Он, безусловно, соответствует базовому типу, но выходит за рамки перечислителей.

Flamefire 05.08.2019 10:15

@Flamefire 3 недействителен для enum {A, B ,C}. 256 действителен для enum : uint16_t{A, B, C}. «Значения перечисления» — это буквально значения, которые вы определяете внутри перечислений. «Значение [то есть] в пределах диапазона значений перечислений» — это значение, равное одному из перечислений. См. упр. перечисление cppreference. Я думаю, что cppreference понимает это лучше всего: If the underlying type is not fixed and the source value is out of range, the result is unspecified (until C++17)undefined (since C++17).

KamilCuk 05.08.2019 11:12

Если я правильно прочитал эту справочную ссылку, то 3 действителен, поскольку диапазон 0..3, внутри которого находится 3. См. также предложение «Обратите внимание, что значение после такого преобразования может не обязательно совпадать с каким-либо из именованных перечислителей, определенных для перечисления», где 7 используется в качестве примера для enum { A = 1, B = 2, C = 4 }.

Flamefire 05.08.2019 11:19

Вы правы, я был неправ. enum{A, B, C} должен иметь диапазон 2 бита, поэтому 3 будет иметь значение. Ключ, кажется, - снова цитируя cppreference - if it would fit in the smallest bit field large enough to hold all enumerators.

KamilCuk 05.08.2019 11:24

Обратите внимание на формулировку сноски 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.

Как «Сноска 100 подтверждает, что это тот случай, когда выход за пределы диапазона представляемых значений является проблемой»? Эта сноска, на мой взгляд, подразумевает только то, что НЕ является проблемой.

jbapple 29.01.2019 15:52

@jbapple «Этот набор значений используется для определения семантики продвижения и преобразования для типа перечисления». Мы имеем дело со значением вне этого набора значений во время семантики продвижения. Отсюда проблема.

JaMiT 29.01.2019 22:50

@jbapple Кроме того, второе предложение сноски 100 не означает, что это не проблема. Я изменил свой ответ, чтобы покрыть это.

JaMiT 30.01.2019 16:43

Авторы стандартов C и C++ не приложили усилий, чтобы полностью указать все случаи, когда все качественные реализации должны вести себя одинаково за исключением случаев, когда у них есть и документально подтверждена веская причина поступать иначе [не обязательно гарантировать что-либо полезное в результирующем поведении]. Для разработчиков компиляторов стало модным ограничивать язык конструкциями, явно определенными в Стандарте, но, по крайней мере, для Стандарта C такие интерпретации прямо противоречат намерениям авторов, как указано в опубликованных документах Rationale.

supercat 30.01.2019 21:09

@supercat У меня проблемы с разбором первого предложения, которое может быть причиной моего вопроса: какое это имеет отношение к моему ответу?

JaMiT 31.01.2019 02:38

@JaMiT: Перечитывая то, что вы написали, я думаю, что в первый раз неправильно понял. Неважно.

supercat 31.01.2019 02:44

Интегральное продвижение не происходит правильно? Почему неопределенное поведение?

xskxzr 01.02.2019 11:59

@xskxzr Это не продвижение или дополнение, которое вызывает носовых демонов; это присвоение результирующего целого числа перечислению.

YSC 01.02.2019 16:45

@JaMiT Я бы предложил выделить ту часть ответа, которая фактически отвечает, удостоверение личности ваша точка зрения о "не исключает".

YSC 01.02.2019 16:46

@YSC Я только что подумал о том же. Кроме того, я придумал пример кода, который лучше иллюстрирует, на что жалуется clang.

JaMiT 01.02.2019 22:04

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

JaMiT 01.02.2019 22:16

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

JaMiT 01.02.2019 22:54

Почему внутренний static_cast в static_cast<bool>(static_cast<A>(8)) не нарушает «Если во время оценки выражения результат не определен математически или не находится в диапазоне представляемых значений для его типа, поведение не определено». Внутреннее выражение вычисляется первым; это тип A, и результат не находится в диапазоне представляемых значений для этого типа, верно?

jbapple 03.02.2019 16:31

@jbapple Ненадежно предполагать, что что-то не является неопределенным поведением просто потому, что UBScan не пометил это как таковое. Это может быть не UB, или это может быть намеренно не описанный случай, или может быть ошибка в UBScan (или, возможно, что-то еще). В данном случае мой предполагать заключается в том, что UBScan проверяет, находится ли значение, загруженное из переменной типа A, в диапазон представляемых значений, но в противном случае не проверяет вычисление выражения типа A.

JaMiT 03.02.2019 19:48

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

return d + B;

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

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

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

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

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


Раздел 7состояния:

For an enumeration whose underlying type is not fixed, the underlying type is an integral type that can represent all the enumerator values defined in the enumeration. If no integral type can represent all the enumerator values, the enumeration is ill-formed. It is implementation-defined which integral type is used as the underlying type except that the underlying type shall not be larger than int unless the value of an enumerator cannot fit in an int or unsigned int. If the enumerator-list is empty, the underlying type is as if the enumeration had a single enumerator with value 0.

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

It is implementation-defined which integral type is used as the underlying type except that the underlying type shall not be larger than int unless the value of an enumerator cannot fit in an int or unsigned int.


Раздел 10состояния:

The value of an enumerator or an object of an unscoped enumeration type is converted to an integer by integral promotion. [ Example:

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

makes color a type describing various colors, and then declares col as an object of that type, and cp as a pointer to an object of that type. The possible values of an object of type color are red, yellow, green, blue; these values can be converted to the integral values 0, 1, 20, and 21. Since enumerations are distinct types, objects of type color can be assigned only values of type 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

Note that this implicit enum to int conversion is not provided for a scoped enumeration:

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

— end example ]

Вот эти две строчки привлекли мое внимание:

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.

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

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

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


-Редактировать-

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

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 генерируются моим компилятором, чтобы увидеть основные действия, которые выполняет компилятор.

Да, это проблема, зависящая от компилятора. Вот почему это вопрос о дезинфицирующем средстве неопределенного поведения Клэнг. Если вы используете clang, но не дезинфицирующее средство (опустите параметр -fsanitize=undefined), пример программы будет работать без ошибок. Если вы используете clang и дезинфицирующее средство, вы получите ошибку и с возвращаемым значением 11. (Ошибка не является фатальной.)

JaMiT 01.02.2019 22:25

@JaMiT О, хорошо, теперь это имеет смысл, я как бы понял это из вопроса ...

Francis Cugler 01.02.2019 23:11

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