Sobes.tech
Middle

Добра идея ли е да използвате типа float като ключ за контейнер?

sobes.tech AI

Отговор от AI

Използването на float като ключ за асоциативни контейнери (например std::map, std::unordered_map) или за директно сортиране не се препоръчва по следните причини:

  • Проблеми с сравняването: Равенството на две числа от тип float (или double) се постига рядко поради грешки в представянето с плаваща запетая. Сравнението a == b може да даде фалшив резултат, дори ако математически числата са равни. Това нарушава инвариантите на контейнерите, които изискват строга слаба подредба (std::map и std::set) или правилно изчисление на хеш и сравнение за еквивалентност (std::unordered_map и std::unordered_set).

  • Неправилно сортиране: Стандартните оператори за сравнение за float не гарантират винаги строга слаба подредба за всички възможни стойности (например NaN).

  • Нестабилен хеш: Реализацията на хеш функциите за float може да бъде нестабилна поради същите проблеми с представянето, което може да доведе до непредсказуемо поведение или ниска производителност на хеш таблиците.

Препоръчителни подходи:

  1. Използване на цялочислено представяне: Ако точността не е критична или числата имат ограничен диапазон и разделителна способност, може да мащабирате и преобразувате float в цяло число (например, int или long long) и да го използвате като ключ.

    float f = 1.23f;
    int key = static_cast<int>(f * 100); // Пример за мащабиране
    std::map<int, Value> my_map;
    my_map[key] = some_value;
    
  2. Използване на фиксирана точка: За случаи, когато е необходима точна репрезентация на дробните числа, може да използвате библиотека за работа с числа с фиксирана точка.

  3. Сравняване с допуск (epsilon): Макар че това не позволява директно използване на float като ключ, при търсене може да сравнявате стойностите с малка толерантност (epsilon).

    bool are_equal(float a, float b, float epsilon = 1e-6) {
        return std::abs(a - b) < epsilon;
    }
    // Не е подходящо за директна употреба като ключ в map
    
  4. Използване на потребителски компаратор (за std::map/std::set): Може да дефинирате потребителски компаратор, който взема предвид толерантността, но това е свързано с рискове за нарушаване на инвариантите на строга слаба подредба.

    struct FloatComparer {
        bool operator()(float a, float b) const {
            // Прост пример, който може да наруши строга слаба подредба
            return a < b - 1e-6;
        }
    };
    // Не се препоръчва за използване в реални приложения
    // std::map<float, Value, FloatComparer> my_map;
    
  5. Използване на битово представяне като цяло число (за std::unordered_map/std::unordered_set): За хеш таблици може да преобразувате битовото представяне на float в цяло число и да го използвате като ключ. Това осигурява уникалност на ключа за всяко уникално битово представяне на float, но не решава проблема с сравняването на плаващи числа, които са математически равни, но имат различно битово представяне (например 0.0 и -0.0).

    union FloatIntConverter {
        float f;
        int i;
    };
    
    struct FloatHash {
        size_t operator()(float f) const {
            FloatIntConverter converter;
            converter.f = f;
            return std::hash<int>()(converter.i);
        }
    };
    
    struct FloatEqual {
        bool operator()(float a, float b) const {
            // Сравнение на битовите представяния
            FloatIntConverter converter_a, converter_b;
            converter_a.f = a;
            converter_b.f = b;
            return converter_a.i == converter_b.i;
        }
    };
    // std::unordered_map<float, Value, FloatHash, FloatEqual> my_map;
    // Внимание: това сравнява битовите представяния, не математическото равенство
    

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