Используя компилятор g++, оба они компилируются нормально:
constexpr double d1 = 2.0 / 1.0;
const double d2 = 2.0 / 0.0;
Но это не так:
constexpr double d2 = 2.0 / 0.0;
Ошибка времени компиляции:
error: ‘(2.0e+0 / 0.0)’ is not a constant expression
Обратите внимание, что cppreference приводит именно этот пример, подтверждая, что это запрещено.
Я не понимаю, почему это не разрешено. Учитывая, что std::numeric_limits::is_iec559 равен constexpr
, компилятор четко знает во время компиляции, в пределах constexpr
, разрешено ли деление на ноль. Итак, если это разрешено... почему это не разрешено в constexpr
?
Обновлено: Было высказано предположение, что этот вопрос аналогичен Поведению деления с плавающей запятой на ноль. Мой вопрос здесь более целенаправленный (почему первые два выражения кажутся разрешенными, а третье — запрещенным)? Ответы на другой вопрос, кажется, не проливают на это света: некоторые ответы подразумевают, что все три выражения являются UB (и, следовательно, запрещены? Я не думаю, что верю в это; но в любом случае есть что-то конкретное). о constexpr, который, кажется, имеет значение, поэтому этот вопрос сосредоточен на constexpr).
Этот вопрос также похож на деление с плавающей запятой во время компиляции на ноль в C++, принятый ответ которого (по сути, «это UB, так что все подходит») вообще не помогает.
@Yksisarvinen нет, это уже не (обязательно) UB; на самом деле это почти никогда не UB. См. stackoverflow.com/questions/42926763/…
Я не могу сказать, что имел в виду cppreference, но стандарт C++ явно говорит, что это неопределенное поведение, без каких-либо условий. eel.is/c++draft/expr.mul#4. И UB не допускается в константных выражениях.
@DonHatch связанный ответ относится к C
документации en.cppreference.com/w/c/language/operator_arithmetic . Я думаю, это место, где C++ и C расходятся.
@Yksisarvinen Интересно. Я предполагал, что cppreference правильно суммирует недавнюю (более позднюю, чем c++11) версию спецификации c++. Возможно нет? Или, возможно, имеется в виду какой-то другой раздел. Теперь я снова в замешательстве.
@DonHatch Как заметил Ричард, вы нашли страницу для языка C. На странице C++ написано: «Если rhs равно нулю, поведение не определено», а затем приступает к объяснению поведения деления на ноль, если std::numeric_limits::is_iec559
истинно... Я не уверен, какое правило здесь задумано, но, как написано, ваш компилятор правильно отклоняет деление на ноль в constexpr.
@RichardCritten ах, понятно, спасибо. Я думаю, что правильная страница для c++ — это en.cppreference.com/w/cpp/language/… . И я думаю, что это перефразирует другая страница cppreference, которую я цитировал. За исключением того, что я, вероятно, неправильно использую термины, и мне нужно скорректировать свое мышление: «иногда UB» на самом деле то же самое, что и «UB», как указано в недавних комментариях к моему ответу stackoverflow.com/questions/42926763/… (а «УБ» не всегда вызывает тревогу).
Также: «7.7 Константные выражения... 5 Выражение E является основным постоянным выражением, если только вычисление E, следуя правилам абстрактной машины (6.9.1), не приведет к вычислению одного из следующих:... (5.7) — операция, которая будет иметь неопределенное поведение, как указано в пунктах 4–15 настоящего документа [Примечание: включая, например, переполнение целых чисел со знаком (7.2), определенную арифметику указателей (7.6.6), деление на ноль (7.6.5) ) или определенные операции сдвига (7.6.7) — последнее примечание] ;" Довольно явно
Этот вопрос похож на: Поведение деления с плавающей запятой на ноль. Если вы считаете, что это другое, отредактируйте вопрос, поясните, чем он отличается и/или как ответы на этот вопрос не помогают решить вашу проблему.
cppreference кажется немного оптимистичным по отношению к IEEE 754: из этого стандарта: 7.3 Деление на ноль 7.3.0 Исключение dividByZero должно сигнализироваться тогда и только тогда, когда для операции с конечными операндами определен точный бесконечный результат. Результатом по умолчанию divByZero должен быть знак ∞, правильно подписанный в соответствии с операцией: - Для деления, когда делитель равен нулю, а делимое представляет собой конечное ненулевое число, знак бесконечности представляет собой исключающее ИЛИ знаков операндов. (см. 6.3)... Я не до конца понимаю смысл. 0/0 там не упоминается
@Эрстед относительно отличия от другого вопроса: я сделал «Обновлено:», пытаясь объяснить. Надеюсь, это имело какой-то смысл.
Действительно, для первого предложенного обмана, а также: stackoverflow.com/questions/30459773/… , stackoverflow.com/questions/68970223/…?
@Oersted Ах, я понимаю. Да, похоже, тот же вопрос. Но принятый ответ на этот вопрос мне мало что проясняет. С другой стороны, я думаю, обсуждение в этой ветке комментариев начинает помогать мне понимать. Не знаю, как действовать дальше. Может быть, если я получу достаточно ясности, я (или кто-то другой) смогу написать четкий и полный ответ на другой вопрос и пометить этот как дубль?
Другой способ увидеть «проблему»: godbolt.org/z/7q6x3Y45E. Именно контекст вычисления константы по праву вызывает ошибку компиляции. В моем фрагменте первая строка (не оцениваемая константой) компилируется, но, я думаю, формально все еще является UB.
@DonHatch: кажется, это правильный путь. Насколько я понимаю, деление на ноль — это UB, и точка. IEEE754 определяет только конечное (!= 0)/0, но не 0/0. Вероятно, это спорно, но на данный момент мой вывод таков: 1- поведение компиляторов соответствует стандарту; 2- формулировка cppref неверна; 3- никогда не полагаться на /0 в своем коде.
кстати: godbolt.org/z/747abWT7z gcc и clang обрабатывают 0/0 по-разному (-nan и nan)
Что касается страницы cppreference, представленной ранее, если плавающая запятая соответствует стандарту IEEE 754, деление на ноль приводит либо к FE_INVALID
, либо к FE_DIVBYZERO
. Если math_errhandling
имеет MATH_ERREXCEPT
, это вызывает исключение с плавающей запятой. Исключения в constexpr
не допускаются. Если для math_errhandling
MATH_ERREXCEPT
установлено значение 0, то исключений нет и можно отправить отчет об ошибке.
@Red.Wave О! Хорошо, я думаю, вы натолкнулись на одно из упомянутых кем-то объяснений, которое на самом деле является для меня убедительным (в отличие от любого из предыдущих аргументов в форме «оно запрещено из-за следующих непонятных немотивированных и, возможно, устаревших отрывков в спецификации C++, чьи содержимое я даже не могу прочитать и должен догадываться из того, что говорит cppreference"). Итак, для меня это убедительно: хотя компилятор знает, что IEEE754 поддерживается, компилятор на самом деле не может просто оценить 1/0 как +бесконечность, потому что на самом деле это может не быть ответом, в зависимости от настроек времени выполнения.
math_errhandling
— это макрос, который расширяется до целочисленного значения основной константы. Я имею в виду, что если эта константа времени компиляции имеет MATH_ERREXCEPT
, то во время компиляции мы знаем, что деление с плавающей запятой на .0
вызывает выброс/повышение. В противном случае мы знаем, что исключений не будет. Таким образом, в первом случае компилятор не должен принимать деление на .0
во время компиляции, а во втором — должен. В противном случае компилятор ведет себя неправильно. Более того, что интересно, в cppreference не указан случай операндов разных типов; поэтому я воздерживаюсь от простого термина div на 0.
@Red.Wave Но en.cppreference.com/w/cpp/numeric/math/math_errhandling говорит: «Если реализация поддерживает арифметику с плавающей запятой IEEE (IEC 60559), math_errhandling & MATH_ERREXCEPT должны быть ненулевыми. " Это заставляет меня думать, что MATH_ERREXCEPT, т. е. «указывает, что используются исключения с плавающей запятой», не означает совсем то, что вы описали выше? Возможно, на уровне этого документа 1./0. всегда выдает FE_DIVBYZERO, тогда среда выполнения ( en.cppreference.com/w/cpp/numeric/fenv) должна решить, следует ли превратить это во что-то еще (бесконечность или прерывание) на более высоком уровне?
(то есть... при условии, что IEEE754 поддерживается)
@DonHatch Что бы ни означало исключение, это ненормальное завершение программы. А этого не может произойти во время компиляции. По сути, это означает, что деления на 0 быть не может constexpr
. Я изначально пропустил абзац, на который вы указали. Спасибо за примечание.
Этот вопрос похож на: Деление плавающей запятой во время компиляции на ноль в C++. Если вы считаете, что это другое, отредактируйте вопрос, поясните, чем он отличается и/или как ответы на этот вопрос не помогают решить вашу проблему.
@Федор, я добавил правку. Этот вопрос на самом деле не отличается от этого, но (как это часто бывает) у другого есть принятый ответ, который, по моему мнению, вообще ничего не решает, поэтому кажется, что потенциал для получения чего-то полезного от этого вопроса есть. застопорился. Напротив, этот текущий вопрос вызывает дискуссию и ответ, который (наряду с текущим/недавним обсуждением в stackoverflow.com/questions/42926763/…) действительно помогает мне понять.
Стандарт ISO C++ явно определяет, что деление на ноль имеет неопределенное поведение как для целочисленных типов, так и для типов с плавающей запятой.
См. [expr.mul]/4.
Если вычисление выражения будет иметь неопределенное поведение в соответствии со спецификацией основного языка, то выражение, вычисление которого будет включать в себя это вычисление, указывается как не являющееся (основным) константным выражением.
Поэтому любое деление на ноль должно быть невозможно во время компиляции.
Хотя деление на ноль не определено стандартом C++, обычно реализации типов с плавающей запятой соответствуют стандарту IEEE 754, который определяет результат деления на ноль. Это, конечно, хорошо. Если программа имеет неопределенное поведение в соответствии со стандартом C++, то компилятор волен придавать ей любую семантику. В частности, он может пообещать соблюдать более строгие правила, например: другая спецификация.
Однако внутри константных выражений язык намеренно ограничен тем, что явно определено стандартом. Компиляторы не могут расширить поведение constexpr
так, как это возможно во время выполнения. Если инициализация переменной constexpr
не является константным выражением, программа имеет неверный формат.
Хотя стандарт никогда не требует от компилятора сбоя компиляции. Если программа неправильно сформирована, единственным требованием является то, что компилятор должен выдать одну диагностику. С таким же успехом это может быть просто предупреждением, и тогда компилятор сможет просто выполнить деление во время компиляции так же, как и во время выполнения.
В определенных контекстах на семантику программы может влиять не только правильность формата, но и то, является ли выражение константным выражением. В этом случае компилятор должен предоставить программе правильную семантику. В этом случае предупреждение и другое поведение были бы невозможны.
Тот факт, что это не постоянное выражение, может быть использован в правильно построенной программе для SFINAE. В противном случае соответствующий компилятор мог бы разрешить это с предупреждением (GCC делает это для некоторых преобразований, которые формально сужают, но очевидно безопасны), но такого рода несогласованность может пугать.
«Это, конечно, хорошо». :-) На самом деле это очень важный момент, который вообще не является общепризнанным. Распространенное (неправильное) понимание, похоже, состоит в том, что на самом деле это не так, то есть UB означает, что соответствующий компилятор не может гарантировать более строгую спецификацию. Таким образом, многие разговоры о UB преждевременно заканчиваются словами «это UB, так что все пойдет», что совершенно бесполезно stackoverflow.com/questions/68970223/… Спасибо за более детальное объяснение; принимая.
Деление на ноль — это неопределенное поведение в C++, даже если ваш компилятор использует IEEE754, который определяет такое поведение.