Странное поведение при сравнении значений класса enum - PullRequest
4 голосов
/ 14 июня 2019

Я работал над некоторым кодом синтаксического анализа JSON, используя прекрасный nlohmann :: json, и для создания полезных сообщений об ошибках я создал функцию для печати типа объекта JSON. Эта функция принимает json::value_t, который является перечислимым классом, определенным в точности следующим образом в json.hpp:

enum class value_t : std::uint8_t {
    null,
    object,
    array,
    string,
    boolean,
    number_integer,
    number_unsigned,
    number_float,
    discarded
};

Вот моя функция. Я передаю ему json::value_t и ожидаю получить строку, описывающую его.

std::string to_string(json::value_t type){
    static const std::map<json::value_t, std::string> mapping = {
        {json::value_t::null,            "null"},
        {json::value_t::object,          "an object"},
        {json::value_t::array,           "an array"},
        {json::value_t::string,          "a string"},
        {json::value_t::boolean,         "a boolean"},
        {json::value_t::number_integer,  "an integer"},
        {json::value_t::number_unsigned, "an unsigned integer"},
        {json::value_t::number_float,    "a floating point number"}
    };
    auto it = mapping.find(type);
    if (it != mapping.end()){
        return it->second;
    }
    return "a mystery value";
}

Но во время отладки в Visual Studio я был действительно напуган, когда эта функция вернула строку "an integer", когда я совершенно точно передал ее json::value_t::number_float.

Опасаясь худшего и желая быстрого исправления, я написал следующую альтернативу, которая идентична, за исключением того, что перечисление всегда приводится к своему базовому типу перед использованием:

std::string to_string_with_cast(json::value_t type){
    using ut = std::underlying_type_t<json::value_t>;
    static const std::map<ut, std::string> mapping = {
        {static_cast<ut>(json::value_t::null),            "null"},
        {static_cast<ut>(json::value_t::object),          "an object"},
        {static_cast<ut>(json::value_t::array),           "an array"},
        {static_cast<ut>(json::value_t::string),          "a string"},
        {static_cast<ut>(json::value_t::boolean),         "a boolean"},
        {static_cast<ut>(json::value_t::number_integer),  "an integer"},
        {static_cast<ut>(json::value_t::number_unsigned), "an unsigned integer"},
        {static_cast<ut>(json::value_t::number_float),    "a floating point number"}
    };
    auto it = mapping.find(static_cast<ut>(type));
    if (it != mapping.end()){
        return it->second;
    }
    return "a mystery value";
}

Это сработало . Прохождение json::value_t::number_float привело к "a floating point number", как я и ожидал.

Все еще любопытный и подозревающий одну из причуд Microsoft или неопределенное поведение, скрывающееся в другом месте в моей довольно большой базе кода, Я провел следующий тест на g ++ :

    std::cout << "Without casting enum to underlying type:\n";
    std::cout << "null:   " << to_string(json::value_t::null) << '\n';
    std::cout << "object: " << to_string(json::value_t::object) << '\n';
    std::cout << "array:  " << to_string(json::value_t::array) << '\n';
    std::cout << "string: " << to_string(json::value_t::string) << '\n';
    std::cout << "bool:   " << to_string(json::value_t::boolean) << '\n';
    std::cout << "int:    " << to_string(json::value_t::number_integer) << '\n';
    std::cout << "uint:   " << to_string(json::value_t::number_unsigned) << '\n';
    std::cout << "float:  " << to_string(json::value_t::number_float) << '\n';

    std::cout << "\nWith casting enum to underlying type:\n";
    std::cout << "null:   " << to_string_with_cast(json::value_t::null) << '\n';
    std::cout << "object: " << to_string_with_cast(json::value_t::object) << '\n';
    std::cout << "array:  " << to_string_with_cast(json::value_t::array) << '\n';
    std::cout << "string: " << to_string_with_cast(json::value_t::string) << '\n';
    std::cout << "bool:   " << to_string_with_cast(json::value_t::boolean) << '\n';
    std::cout << "int:    " << to_string_with_cast(json::value_t::number_integer) << '\n';
    std::cout << "uint:   " << to_string_with_cast(json::value_t::number_unsigned) << '\n';
    std::cout << "float:  " << to_string_with_cast(json::value_t::number_float) << '\n';
}

И я действительно испугался, увидев то же поведение, что и Visual Studio:

Without casting enum to underlying type:
null:   null
object: an object
array:  an array
string: a string
bool:   a boolean
int:    an integer
uint:   an integer
float:  an integer
With casting enum to underlying type:
null:   null
object: an object
array:  an array
string: a string
bool:   a boolean
int:    an integer
uint:   an unsigned integer
float:  a floating point number

Почему это происходит? Похоже, что number_float и number_unsigned считаются равными number_integer. Но согласно этому ответу нет ничего особенного в сравнении нормального enum. Есть ли что-то особенное в использовании enum class? Это стандартное поведение?


РЕДАКТИРОВАТЬ: Вот гораздо более простой источник путаницы: Похоже, что если я использую < для сравнения любой пары последних трех значений класса перечисления , он всегда возвращает false. Это, вероятно, суть моей проблемы выше. Почему это странное поведение? Следующий вывод взят из этого живого примера

number_integer  < number_integer  : false
number_integer  < number_unsigned : false
number_integer  < number_float    : false
number_unsigned < number_integer  : false
number_unsigned < number_unsigned : false
number_unsigned < number_float    : false
number_float    < number_integer  : false
number_float    < number_unsigned : false
number_float    < number_float    : false
null            < number_integer  : true
null            < number_unsigned : true
null            < number_float    : true
bool            < number_integer  : true
bool            < number_unsigned : true
bool            < number_float    : true

1 Ответ

7 голосов
/ 14 июня 2019

У вас есть эта проблема, потому что для этого перечисления предусмотрено operator<:

inline bool operator<(const value_t lhs, const value_t rhs) noexcept
{
    static constexpr std::array<std::uint8_t, 8> order = {{
            0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */,
            1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */
        }
    };

    const auto l_index = static_cast<std::size_t>(lhs);
    const auto r_index = static_cast<std::size_t>(rhs);
    return l_index < order.size() and r_index < order.size() and order[l_index] < order[r_index];
}

из здесь

И в соответствии с этим кодом integer,unsigned и float считаются равными, следовательно, ваша проблема.

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

...