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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With