Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bizarre behavior when comparing enum class values

I was working on some JSON parsing code using the lovely nlohmann::json, and to help make useful error messages, I made myself a function to print the type of a JSON object. This function accepts a json::value_t, which is an enum class defined exactly as follows in json.hpp:

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

Here's my function. I pass it a json::value_t and I expect to receive a string that describes it.

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";
}

But, while debugging in Visual Studio, I was really spooked when this function returned the string "an integer" when I very certainly passed it json::value_t::number_float.

Fearing the worst, and wanting a quick fix, I wrote the following alternative, which is identical except that the enum is always cast to its underlying type before it is used:

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";
}

This worked. Passing a json::value_t::number_float resulted in "a floating point number", as I expected.

Still curious, and suspecting one of Microsoft's quirks or Undefined Behavior lurking elsewhere in my fairly large code base, I ran the following test on 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';
}

And I was really spooked to see the same behavior as 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

Why is this happening? It appears that number_float and number_unsigned are both considered equal to number_integer. But according to this answer, there is nothing special about comparing a normal enum. Is anything different about using an enum class? Is this standard behavior?


EDIT: Here's a much simpler source of confusion: It appears that if I use < to compare any pair of the last three enum class values, it always returns false. This is probably the heart of my issue above. Why is there this strange behavior? The following output is from this live example

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
like image 566
alter_igel Avatar asked Mar 04 '23 20:03

alter_igel


1 Answers

You have this issue because there is operator< provided for this enum:

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];
}

from here

And according to this code integer, unsigned and float are considered to be equal, hense your issue.

As a solution you can use your method or simply replace default comparator with lambda or provide specialization for std::less which is not using this operator.

like image 65
Slava Avatar answered Mar 22 '23 23:03

Slava