【问题标题】:Bizarre behavior when comparing enum class values比较枚举类值时的奇怪行为
【发布时间】:2019-06-14 01:12:09
【问题描述】:

我正在使用可爱的 nlohmann::json 处理一些 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 中调试时,当我非常肯定地传递了 json::value_t::number_float 时,这个函数返回了字符串 "an integer",我真的被吓到了。

担心最坏的情况,并想要快速修复,我编写了以下替代方案,除了枚举在使用之前始终转换为其基础类型之外,它是相同的:

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"

仍然好奇,并怀疑微软的怪癖或未定义行为之一潜伏在我相当大的代码库的其他地方,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';
}

看到与 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_floatnumber_unsigned 都被认为等于number_integer。但是根据this answer 的说法,比较普通的enum 并没有什么特别之处。使用enum class 有什么不同吗?这是标准行为吗?


编辑:这是一个更简单的混淆来源: 看来,如果我使用&lt; 比较任何一对最后三个枚举类值,它总是返回false。这可能是我上面问题的核心。为什么会有这种奇怪的行为?以下输出来自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

【问题讨论】:

  • 您可能错过了value_t 的免费operator&lt;
  • 发布Minimal, Reproducible Example而不是一堆sn-ps会改善问题
  • @RetiredNinja 你是对的。我应该在源代码中进一步向下滚动。
  • @RetiredNinja 哎哟......看起来像是一个通常被认为是精心设计的库的错误(提供operator&lt;,它没有给出严格的弱排序)
  • @M.M 实际上它给出了严格的弱排序,它只是认为这 3 个是相等的。

标签: c++ string enums nlohmann-json


【解决方案1】:

您遇到此问题是因为为此枚举提供了operator&lt;

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

来自here

并且根据此代码integerunsignedfloat 被认为是相等的,请提出您的问题。

作为一种解决方案,您可以使用您的方法或简单地将默认比较器替换为 lambda,或为未使用此运算符的 std::less 提供专业化。

【讨论】:

  • 这种排序的原因是数字的确切类型可以由解析器确定,因此对库的用户是透明的。因此,我希望所有数字的行为都相同,以避免出现意外。
猜你喜欢
  • 2012-11-15
  • 2017-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多