Являются ли IEEE-типы действительными ключами для std :: map и std :: set? - PullRequest
20 голосов
/ 27 января 2011

Фон

Требование к компаратору для типа ключа ассоциативного контейнера (например, std :: map) заключается в том, что он накладывает строгий слабый порядок на элементы типа ключа.

Для данного компаратора comp(x, y) мы определяем equiv(x, y) = !comp(x, y) && !comp(y, x).
Требования к comp(x, y) строгому слабому порядку

  1. Необратимость (!comp(x, x) для всех x)
  2. Транзитивность порядка (если comp(a, b) и comp(b, c), то comp(a, c)).
  3. Транзитивность эквивалентности (если equiv(a, b) и equiv(b, c), то equiv(a, c))

std::less<float> (компаратор по умолчанию) использует operator<, что не создает строгий слабый порядок из-за NaN. Поскольку x < NaN и NaN < x являются ложными для всех x, NaN эквивалентно всем числам с плавающей запятой в этом компараторе, это нарушает условие # 3: equiv(1.0, NaN) и equiv(NaN, 2.0), но не equiv(1.0, 2.0). Для чисел с плавающей запятой IEEE, кроме NaN, это строгий слабый порядок (где каждое число имеет свой собственный класс эквивалентности, кроме 0 и -0).

Вопрос

Означает ли это, что по стандарту C ++ нельзя использовать плавающие элементы IEEE (и (длинные) удваиваются) в качестве типа ключа в ассоциативном контейнере из-за вышеуказанной проблемы, даже если я удостоверяюсь, что NaN никогда не вставляется в контейнер? Я не совсем уверен насчет формулировки «элементы Key» в стандарте - означает ли это все возможные элементы или только те элементы, которые попадают в контейнер.

Примечание. Вопрос не в вопросах. усечение / округление, скорее всего, скоро я опубликую другой вопрос.

Обновление:

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

Вопрос на самом деле : разрешено ли использовать компаратор, который только налагает строгий слабый порядок на элементы, помещаемые в контейнер, а не на все возможные экземпляры типа ключа? Пожалуйста, не просто отвечайте «да» или «нет», я хотел бы получить некоторые ссылки на стандартные / предыдущие обсуждения этого / ответ от члена комитета или что-то в этом роде.

Ответы [ 4 ]

9 голосов
/ 27 января 2011

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

Аналогичный случай: что если компаратор (для контейнера указателей или интеллектуальных указателей) вызывает виртуальную функцию, и кто-то связывает производный класс сравниваемого типа, который переопределяет виртуальную функцию таким образом, что компаратор не строгий слабый порядок? Не становится ли программа неопределенной, даже если никто не использует этот производный класс?

Если сомневаетесь, вы можете поддержать NaN с помощью компаратора, который является строгим слабым порядком:

bool operator()(double a, double b) {
    if ((a == a) && (b == b)) {
        return a < b;
    }
    if ((a != a) && (b != b)) return false;
    // We have one NaN and one non-NaN.
    // Let's say NaN is less than everything
    return (a != a)
}

Последние две строки "оптимизируются" до return (b == b);, хотя я не уверен, что комментарий оптимизирует с ним.

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

Это не имеет особого смысла, так как карта не вызывает значений из ниоткуда, она использует только те значения, которые ей даны (и копии их), но вопрос касается правил, и их правила, насколько я знать. C ++ 0x - то же самое. Интересно, есть ли отчет о дефекте или какой-либо пункт, отправляющий его.

Это также раздражает в тех (очень редких) системах, где std::less медленно для указателей, вы не можете использовать < в качестве компаратора на карте указателей, даже если вы знаете что все указатели на элементы одного и того же массива. Позор.

Другой вариант - использовать следующий класс в качестве типа ключа, поэтому ключи проверяются на наличие NaN только при входе в карту, а не при каждом сравнении, как указано выше.

struct SaneDouble {
    double value;
    SaneDouble (double d) : value(d) {
        if (d != d) throw std::logic_error();
    }
    static friend bool operator<(SaneDouble lhs, SaneDouble rhs) {
        return lhs.value < rhs.value;
    }
    // possibly a conversion to double
};

Это поднимает другой вопрос - очевидно, что кто-то может создать SaneDouble, а затем установить для value значение NaN (при условии, что реализация позволяет получить его откуда-то без сбоев). Так "элементы SaneDouble" строго-слабо упорядочены или нет? Неужели моя нерешительная попытка создать инвариант класса в конструкторе делает мою программу неопределенной, даже если никто не нарушает инвариант просто потому, что они могут и, следовательно, результатом этого являются "элементы SaneDouble"? Действительно ли цель стандарта состоит в том, чтобы поведение программы определялось тогда и только тогда, когда value помечено private? Действительно ли стандарт определяет где-нибудь, что такое "элементы" типа?

Интересно, следует ли нам интерпретировать «элементы ключа», чтобы обозначить, что компаратор налагает строгий слабый порядок на некоторые элементы ключа. Предположительно, включая те, которые действительно используются. «У меня есть пончики» не означает, что у меня есть каждый пончик. Хотя это натянуто.

2 голосов
/ 27 января 2011

Короче говоря: это нормально (в том смысле, в каком идет речь о вашем вопросе).

Если вы запретите значения (т. Е. NaN), которые не удовлетворяют требованиям заказа, то поведениеполностью определено.

1 голос
/ 27 января 2011

Размещение float в качестве ключей ассоциативного контейнера иногда является плохой идеей, так как семантика равенства довольно плохая. Но это зависит от того, что вы хотите сделать. Имейте в виду, что NaN и бесконечности обычно не являются проблемой. Вы можете обрабатывать их с помощью специальных функций компаратора (обычно я этого не делаю), и, очевидно, требования стандарта касаются фактических ключей, которые окажутся в контейнере , которые вы можете видеть как подмножество типа ключа. Реализация карты никогда не представит ключевые экземпляры, которые вы не добавили в карту.

Я однажды использовал этот предикат для карты, где я мог запретить два очень близких значения:

struct FuzzyComparer
{
    template <typename T>
    bool operator()(T x, T y) const
    {
        static const T oneMinusEps = (T)1. - 64 * std::numeric_limits<T>::epsilon();
        return x / y < oneMinusEps;
    }
};

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

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

Вы должны либо отбросить часть «отношения эквивалентности», которая не должна быть большой проблемой в коде реального мира (я сомневаюсь, что транзитивность отношения eq. Используется в той степени, которая мешала бы вам в типичная реализация карты) или выбросить совместимость с арифметическими операциями. Но какой смысл использовать значения с плавающей запятой в качестве ключей?

0 голосов
/ 27 января 2011

Вы можете поместить числа с плавающей запятой и двойники в качестве ключа для std :: map или std :: set с целью их правильной сортировки.

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

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

«lookup» может не найти элемент, который там есть, если вы используете простой «find», поэтому вы можете захотеть использовать некоторый вид поиска epsilon с upper_bound () в x-delta, где x - это то, что вы действительно ищете для, и позволяя значение, которое меньше, чем х + дельта.

Учитывая все это, очевидно, нет проблем с использованием float или double в качестве ключа в std :: multiset или std :: multimap, если вы используете upper_bound для поиска, а не equal_range.

Что касается NaN, если набор или карта не пустые, они будут считаться "равными" любому элементу, который уже существует, и поэтому не будут вставлены. Если вы сначала вставите NaN, все последующие вставки будут неудачными.

Они будут считаться равными, потому что x<NaN и NaN<x оба оценивают как ложные.

Конечно, по той же логике, если это карта и вы звоните

myMap[ NaN ] = x;

это может оправданно изменить любой элемент.

(То же самое, если вы делаете find(NaN), который может вернуть любой итератор).

Поэтому, если вы собираетесь использовать NaN в какой-либо части этого вычисления, используйте специальный компаратор, такой как Стив Джессоп.

...