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

«Стоит ли использовать float в качестве ключа ассоциативного контейнера?» — вопрос из категории STL, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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