Стоит ли использовать float в качестве ключа ассоциативного контейнера?

Ответ

Нет, это крайне не рекомендуется для std::map, std::set, std::unordered_map и std::unordered_set.

Причины:

  1. Проблемы точности: Из-за двоичного представления чисел с плавающей точкой операции могут давать разные результаты на разных архитектурах или с разными уровнями оптимизации. Значение, вычисленное как 0.1 + 0.2, не будет бинарно равно литералу 0.3.
  2. Ненадёжное сравнение: std::map использует operator< для упорядочения. Для float это приводит к недетерминированному поведению из-за погрешностей.
  3. Неподходящее хеширование: std::unordered_map требует строгого равенства ключей. Два математически равных float могут иметь разные битовые представления и, следовательно, разные хеши.

Альтернативы, которые я использовал:

  • Фиксированная точка: Умножить на коэффициент и использовать целочисленный тип.
    std::map<int, std::string> priceMap; // Цена в рублях с копейками
    float price = 123.45f;
    priceMap[static_cast<int>(price * 100)] = "Товар"; // Ключ: 12345
  • Кастомный компаратор с эпсилон-окрестностью (осторожно, нарушает строгое слабое упорядочение):
    struct FloatCompare {
    bool operator()(float a, float b) const {
        const float eps = 1e-6f;
        if (std::abs(a - b) < eps) return false; // Считаем равными
        return a < b;
    }
    };
    // std::map<float, Value, FloatCompare> map; // Рискованно
  • Использовать std::ratio или библиотеки decimal для финансовых расчётов.

В production-коде я всегда избегаю float/double как ключей, предпочитая целочисленные или строковые представления.

Ответ 18+ 🔞

Бля, слушай, тут один чувак на днях спрашивает: "А можно ли использовать float как ключ в map?" Я ему так сразу: "Нет, ёпта, это крайне не рекомендуется!" И не только для std::map, а вообще для всех этих std::set, std::unordered_map и std::unordered_set. Вообще забей.

А почему, спросишь? Да овердохуища причин!

  1. Проблемы с точностью — пиздец какие. Из-за того, как числа с плавающей точкой внутри устроены, одна и та же операция на разных железяках или с разными флагами компилятора может дать слегка разные результаты. Ну вот представь: 0.1 + 0.2 в компьютере бинарно — это не то же самое, что просто 0.3. И что ты с этим будешь делать?
  2. Сравнение работает через жопу. Обычный std::map для порядка использует operator<. А для float это сравнение из-за этих самых погрешностей становится каким-то нестабильным, поведение недетерминированное получается. Один раз ключ найдётся, другой раз — хуй с горы.
  3. Хеширование вообще в пизду. Для std::unordered_map нужно, чтобы ключи либо были равны, либо нет. А два математически одинаковых floatа могут в памяти лежать по-разному. Хеш у них будет разный, и твой ключ в контейнере просто потеряется, манда с ушами.

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

  • Фиксированная точка — мой любимый способ. Берёшь, умножаешь всё на какой-нибудь коэффициент (например, на 100 для копеек) и используешь уже нормальный целочисленный ключ. Просто и надёжно.
    std::map<int, std::string> priceMap; // Цена в рублях с копейками
    float price = 123.45f;
    priceMap[static_cast<int>(price * 100)] = "Товар"; // Ключ: 12345
  • Кастомный компаратор с эпсилон-окрестностью. Это уже для отчаянных. Делаешь свою функцию сравнения, которая считает числа равными, если они отличаются меньше чем на какую-то мелкую величину (эпсилон). Но тут, чувак, осторожно — можно нарушить строгое слабое упорядочение, и тогда твой map просто сломается в самый неподходящий момент.
    struct FloatCompare {
    bool operator()(float a, float b) const {
        const float eps = 1e-6f;
        if (std::abs(a - b) < eps) return false; // Считаем равными
        return a < b;
    }
    };
    // std::map<float, Value, FloatCompare> map; // Рискованно, как ходить по охуенно тонкому льду
  • Использовать std::ratio или специальные библиотеки для decimal-типов. Особенно если дело касается денег — там без вариантов.

Короче, вывод простой: в нормальном, production-коде я всегда обхожу float/double как ключи стороной. Лучше уж преобразовать во что-то целочисленное или строку, чем потом охуевать от необъяснимых багов. Доверия к этому подходу — ноль ебать.