Приводят ли std :: min (0.0, 1.0) и std :: max (0.0, 1.0) к неопределенному поведению? - PullRequest
50 голосов
/ 14 марта 2019

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

Краткое чтение :

(IEEE 754) double не Cpp17LessThanComparable , поскольку < не является строгим слабым отношением упорядочения из-за NaN. Следовательно, Требуется элементы std::min<double> и std::max<double> нарушены.

Длинное чтение :

Все ссылки следуют n4800 . Характеристики std::min и std::max приведены в 24.7.8:

template<class T> constexpr const T& min(const T& a, const T& b);
template<class T> constexpr const T& max(const T& a, const T& b);
Требуется: [...] тип T должен быть Cpp17LessThanComparable (Таблица 24).

Таблица 24 определяет Cpp17LessThanComparable и говорит:

Требование: < - строгое слабое отношение порядка (24.7)

Раздел 24.7 / 4 определяет строгий слабый порядок . В частности, для < говорится, что «если мы определим equiv(a, b) как !(a < b) && !(b < a), то equiv(a, b) && equiv(b, c) подразумевает equiv(a, c)».

Теперь, согласно IEEE 754 equiv(0.0, NaN) == true, equiv(NaN, 1.0) == true, но equiv(0.0, 1.0) == false, мы заключаем, что < является , а не строгим слабым порядком. Следовательно, (IEEE 754) double является , а не Cpp17LessThanComparable , что является нарушением условий Требуется std::min и std::max.

Наконец, 15.5.4.11/1 говорит:

Нарушение любых предварительных условий, указанных в функции Требуется: элемент приводит к неопределенному поведению [...].

Обновление 1:

Суть вопроса не в том, чтобы утверждать, что std::min(0.0, 1.0) не определено, и что-либо может произойти, когда программа оценивает это выражение. Возвращает 0.0. Период. (Я никогда не сомневался в этом.)

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

Посмотрите, например, определение в Википедии строгого слабого порядка . Он содержит четыре маркера, и каждый из них начинается с «Для каждого x [...] в S ...». Никто из них не говорит «Для некоторых значений x в S, которые имеют смысл для алгоритма» (Какой алгоритм?). Кроме того, в спецификации std::min ясно сказано, что «T должно быть Cpp17LessThanComparable », что влечет за собой то, что < является строгим слабым порядком для T. Следовательно, T играет роль множества S на странице Википедии, и четыре маркера должны сохраняться, когда значения T рассматриваются полностью.

Очевидно, что NaN - это совершенно разные звери от других двойных значений, но они все же возможные значения. Я не вижу ничего в Стандарте (который является довольно большим, 1719 страниц, и, следовательно, этот вопрос и тег языка-юриста), что математически приводит к выводу, что std::min хорошо с двойными числами при условии, что NaN не участвуют.

На самом деле, можно утверждать, что с NaN все в порядке, и другие двойники - это проблема! Действительно, напомним, что существует несколько возможных двойных значений NaN (2 ^ 52 - 1 из них, каждое из которых несет различную полезную нагрузку). Рассмотрим множество S, содержащее все эти значения и один «нормальный» дубль, скажем, 42.0. В символах S = {42.0, NaN_1, ..., NaN_n}. Оказывается, < - строгий слабый порядок на S (доказательство оставлено читателю). Был ли этот набор значений, который имел в виду Комитет C ++ при указании std::min, как в «пожалуйста, не используйте никакие другие значения, в противном случае строгое слабое упорядочение нарушено и поведение std::min не определено»? Могу поспорить, что это не так, но я бы предпочел прочитать это в Стандарте, чем размышлять, что означают «некоторые значения».

Обновление 2:

Сравните декларацию std::min (выше) с декларацией clamp 24.7.9:

template<class T> constexpr const T& clamp(const T& v, const T& lo, const T& hi);
Требуется: Значение lo не должно превышать hi.Для первой формы тип T должен быть Cpp17LessThanComparable (Таблица 24).[...]
[Примечание: если избежать NaN, T может быть типом с плавающей запятой.- конец примечания]

Здесь мы ясно видим что-то, что говорит: "std::clamp хорошо с двойными числами при условии, что NaN не участвуют".Я искал предложение того же типа для std::min.

Стоит обратить внимание на параграф [structure.requirements] / 8, который Барри упомянул в своем посте .По-видимому, это было добавлено после C ++ 17 из P0898R0 ):

Обязательные операции любой концепции, определенной в этом документе, не обязательно должны быть полными функциями;то есть некоторые аргументы требуемой операции могут привести к тому, что требуемая семантика не будет удовлетворена.[Пример: требуемый оператор < концепции StrictTotallyOrdered (17.5.4) не удовлетворяет семантическим требованиям этой концепции при работе с NaN.- конец примера] Это не влияет на то, удовлетворяет ли тип концепции.

Это явная попытка решить проблему, которую я поднимаю здесь, но в контексте концепций (и как указаноБарри, Cpp17LessThanComparable не является концепцией).Кроме того, ИМХО этому пункту тоже не хватает точности.

Ответы [ 3 ]

11 голосов
/ 17 марта 2019

В новом [concepts.equality] в несколько ином контексте имеем:

Выражение является сохраняющим равенство , если при равных входных данных выражение приводит к равным выходным данным. Входные данные для выражения являются набором операндов выражения. Выходные данные выражения являются результатом выражения и всеми операндами, измененными выражением.

Не все входные значения должны быть действительными для данного выражения; например, для целых чисел a и b выражение a / b не является четко определенным, когда b равно 0. Это не исключает, что выражение a / b сохраняет равенство. домен выражения - это набор входных значений, для которых требуется, чтобы выражение было четко определено.

Хотя это понятие области выражения не полностью выражено в стандарте, это единственное разумное намерение: синтаксические требования - это свойства типа, семантические требования - свойства фактических значений.

В более общем смысле у нас также есть [struct.requirements] / 8 :

Требуемые операции любой концепции, определенной в этом документе, не обязательно должны быть полными функциями; то есть некоторые аргументы требуемой операции могут привести к тому, что требуемая семантика не будет удовлетворена. [ Пример : Требуемый оператор < концепции StrictTotallyOrdered ([concept.stricttotallyordered]) не соответствует семантическим требованиям этой концепции при работе на NaN s. - end example ] Это не влияет на то, удовлетворяет ли тип концепции.

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

<ч />

Когда Cpp17LessThanComparable дает семантическое требование, что

< - строгое слабое отношение порядка (24.7)

Единственный способ для этого - предоставить пару значений, которые нарушают требования строгого слабого порядка. Для типа, подобного double, это будет NaN. min(1.0, NaN) - неопределенное поведение - мы нарушаем семантические требования алгоритма. Но для чисел с плавающей запятой без NaN, < - это строгий слабый порядок - это нормально ... вы можете использовать min, max, sort, все, что вам нравится.

В дальнейшем, когда мы начнем писать алгоритмы, использующие operator<=>, это понятие домена является одной из причин того, что выражение синтаксического требования ConvertibleTo<decltype(x <=> y), weak_ordering> было бы неправильным требованием. Имея x <=> y be partial_ordering - это хорошо, просто он видит пару значений, для которых x <=> y равен partial_ordering::unordered (а это, по крайней мере, мы могли бы диагностировать через [[ assert: (x <=> y) != partial_ordering::unordered ]];)

6 голосов
/ 20 марта 2019

Отказ от ответственности: я не знаю полного стандарта C ++, я немного изучил то, что было сказано о поплавках.Я знаю о IEEE 754-2008 и числах с плавающей запятой и C ++.

Да, вы правы, это неопределенное поведение по стандарту C ++ 17.

Краткое чтение:

Стандарт не говорит, что std::min(0.0, 1.0); - это неопределенное поведение, он говорит, что constexpr const double& min(const double& a, const double& b); - это неопределенное поведение.Это означает, что не применяется функция, которая не определена, это само объявление функции , которое не определено.Как и в случае с математикой: минимальная функция невозможна для полного диапазона чисел с плавающей точкой IEEE 754. Как вы отметили,

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

Почему вы не должны использовать std::min on double:

Поскольку я понимаю, что следующий раздел длинного чтения может стать скучным, вот забавный пример риска использования NaN внутри сравнений (я даже не пробовал сортировать алгоритмы…):

#include <iostream>
#include <cmath>
#include <algorithm>

int main(int, char**)
{
    double one = 1.0, zero = 0.0, nan = std::nan("");

    std::cout << "std::min(1.0, NaN) : " << std::min(one, nan) << std::endl;
    std::cout << "std::min(NaN, 1.0) : " << std::min(nan, one) << std::endl;

    std::cout << "std::min_element(1.0, 0.0, NaN) : " << std::min({one, zero, nan}) << std::endl;
    std::cout << "std::min_element(NaN, 1.0, 0.0) : " << std::min({nan, one, zero}) << std::endl;

    std::cout << "std::min(0.0, -0.0) : " << std::min(zero, -zero) << std::endl;
    std::cout << "std::min(-0.0, 0.0) : " << std::min(-zero, zero) << std::endl;
}

При компиляции на моем macbookpro с Apple LLVM версии 10.0.0 (clang-1000.10.44.4) (я делаю точность, потому что, это неопределенное поведение, так что этоможет теоретически иметь другие результаты на других компиляторах) Я получаю:

$ g++ --std=c++17 ./test.cpp
$ ./a.out
std::min(1.0, NaN) : 1
std::min(NaN, 1.0) : nan
std::min_element(1.0, 0.0, NaN) : 0
std::min_element(NaN, 1.0, 0.0) : nan
std::min(0.0, -0.0) : 0
std::min(-0.0, 0.0) : -0

Это означает, что вопреки тому, что вы можете предположить, std::min не является симметричным , когда задействованы NaN, иличетный -0.0.И NaNs не размножаются.Короткая история: Это вызвало у меня некоторую боль в предыдущем проекте, где мне пришлось реализовать собственную функцию min для правильного распространения NaN с обеих сторон, как того требовала спецификация проекта.Поскольку std::min для двойных чисел не определено !

IEEE 754:

Как вы заметили, числа IEEE 754 с плавающей запятой (или ISO / IEC / IEEE 60559: 2011-06, который является нормой, используемой стандартом C11 (см. ниже, где более или менее копируются IEEE754 для языка C), не имеет строгого слабого порядка, поскольку NaNs нарушает транзитивность несопоставимости ( четвертый пункт страницы Википедии )

Самое интересное, что норма IEE754 была пересмотрена в 2008 году (теперь она называется IEEE-754-2008), которая включает в себя общую функцию заказа .Дело в том, что и C ++ 17, и C11 не реализуют IEE754-2008, а скорее ISO / IEC / IEEE 60559: 2011-06

Но кто знает?Возможно, это изменится в будущем.

Длинное чтение:

Во-первых, давайте начнем с напоминания того, что на самом деле является неопределенным поведением, из того же стандартного черновика, который высвязанный (выделено мной):

неопределенное поведение, для которого данный документ не предъявляет никаких требований

[Примечание 1 к записи: Неопределенное поведение может ожидаться приэтот документ опускает любое явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные.Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей или без выдачи диагностического сообщения), до прекращения переводаили выполнение (с выдачей диагностического сообщения).Многие ошибочные программные конструкции не порождают неопределенное поведение;они должны быть диагностированы.Оценка константного выражения никогда не демонстрирует поведение, явно указанное как неопределенное в пунктах с 4 по 14 данного документа (7.7).—Конечная записка]

Нет такой вещи, как «уступать» неопределенному поведению.Это просто то, что не определено в стандарте C ++.Это может означать, что вы можете использовать его и получить правильный результат на свой страх и риск (например, выполнив std::min(0.0, 1.0); Или это может вызвать предупреждение или даже ошибки компиляции, если вы найдете компилятор, который действительно осторожен с числами с плавающей запятой!

О подмножестве ... Вы говорите:

Я не вижу ничего в Стандарте (довольно большом, 1719 страниц, и, следовательно, этот вопрос и тег языка-юриста), который математически приводитк выводу, что std :: min подходит для удвоений при условии, что NaN не задействованы.

Я тоже не читал стандарт, но из той части, которую вы опубликовали, кажется, стандарт ужеговорит, что это нормально. Я имею в виду, если вы создадите новый тип T , который оборачивает двойные значения, исключая NaN, тогда определение template<class T> constexpr const T& min(const T& a, const T& b); , примененное к вашему новому типу , будет иметь определенныйповедение и вести себя точно так, как вы ожидаете от минимальной функции.

Мы также могли бы взглянуть на стандартное определение операции < ondouble, который определен в разделе 25.8 Математические функции для типов с плавающей запятой , что говорит не очень полезно:

Функции классификации / сравнения ведут себя так же, какМакросы C с соответствующими именами, определенными в стандартной библиотеке C.Каждая функция перегружена для трех типов с плавающей точкой.См. Также: ISO C 7.12.3, 7.12.4

Что говорит стандарт C11 ?(Поскольку я предполагаю, что C ++ 17 не использует C18)

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

Что касается использования нормы C11, то оно находится в приложении F этой нормы:

В этом приложении указана поддержка языка C для стандарта IEC 60559 с плавающей запятой.Стандарт МЭК 60559 - это, в частности, двоичная арифметика с плавающей точкой для микропроцессорных систем, второе издание (МЭК 60559: 1989), ранее обозначенная МЭК 559: 1989 и в качестве стандарта IEEE для двоичной арифметики с плавающей точкой (ANSI / IEEE 754−1985).Стандарт IEEE для радикально-независимой арифметики с плавающей точкой (ANSI / IEEE854-1987) обобщает бинарный стандарт для удаления зависимостей от оснований и длины слова.МЭК 60559, как правило, относится к стандарту с плавающей запятой, как при работе с МЭК 60559, формате МЭК 60559 и т. Д.

3 голосов
/ 17 марта 2019

Единственно возможная (не просто правдоподобная) интерпретация состоит в том, что уравнения применяются к значениям в диапазоне функции;то есть значений, фактически используемых в алгоритмах .

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

Это не проблема здесь .

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

РЕДАКТИРОВАТЬ:

Тип не определяет набор значений для алгоритма.Это очевидно для пользовательских типов данных, которые имеют внутренние инварианты, которые формально не указаны ни в одном коде.

Набор значений, которые можно использовать в любом контейнере, алгоритм (контейнеры используют алгоритмы на элементах для внутренних целей) ...свойство этого конкретного использования этого контейнера или алгоритма.Эти библиотечные компоненты не имеют общих элементов: если у вас есть два set<fraction> S1 и S2, их элементы не будут использоваться другим: S1 будет сравнивать элементы в S1, S2 будет сравнивать элементы в S2.Два набора существуют в разных «вселенных», и их логические свойства изолированы.Инварианты держатся для каждого независимо; если вы вставите в S2 элемент x2, который не меньше или больше, чем x1 в S1 (то есть считается эквивалентным), вы не ожидаете, что x2 будет найден в месте x1 в S1! Нетвозможное совместное использование структур данных между контейнерами и элементами не может быть разделено между алгоритмами (у которых не может быть статических переменных типа шаблона, поскольку у него было бы неожиданное время жизни).

Иногда стандарт - это загадка, где выдолжен найти правильное толкование (наиболее правдоподобное, наиболее полезное, наиболее вероятное предназначение);в случае, если членов комитета попросят прояснить вопрос, они остановятся на наиболее X интерпретации (X = правдоподобно, полезно ...), даже если это противоречит точной предыдущей формулировке, поэтому, когда текст неясен или дает сумасшедшие выводы, выможет также пропустить буквальное чтение и перейти к самому полезному.

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

Вы не ожидаете, что vector<int*> будет недопустимым, поскольку указатели могут иметь недопустимые значения, которые нельзя скопировать: только использование такого значения недопустимо.

Таким образом

vector<int*> v;
v.push_back(new int);
vector<int*> v2 = v; // content must be valid
delete v[0];
v[0] = null; // during v[0] invocation (int*)(v[0]) has no valid value

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

В этом случае мы можем вызвать функцию-член вектора, знаячто его элементы не соответствуют концепции Assignable, потому что не допускается присваивание, как гарантия без исключенияне разрешать: значение, хранящееся в v[0], не может использоваться v[0], пользовательская операция над элементом, разрешенным в vector<>::operator[], не предусмотрена. * Компоненты библиотеки могут использовать только определенныеоперации, упомянутые в описании конкретной функции над значениями, используемыми в этом вызове;даже для встроенного типа он не может создавать значения другими способами: конкретный экземпляр set<int,comp> может не сравнивать значения с 0, если 0 не вставлен или не найден в конкретном экземпляре, поскольку 0 может даже не находиться в домене.из comp.

Таким образом, встроенные или типы классов здесь обрабатываются единообразно .Реализация библиотеки не может принимать что-либо на множестве значений, даже если она создается с помощью встроенных типов.

...