当前位置: 首页 > 知识库问答 >
问题:

通过C++20三元比较,更多的沉默行为改变

郑晗日
2023-03-14

令我吃惊的是,我遇到了另一个障碍,比如C++20行为破坏了现有代码的相等运算符?。

考虑使用一个简单的不区分大小写的键类型,例如,std::setstd::map:

// Represents case insensitive keys
struct CiKey : std::string {
    using std::string::string;
    using std::string::operator=;

    bool operator<(CiKey const& other) const {
        return boost::ilexicographical_compare(*this, other);
    }
};

简单测试:

using KeySet   = std::set<CiKey>;
using Mapping  = std::pair<CiKey, int>; // Same with std::tuple
using Mappings = std::set<Mapping>;

int main()
{
    KeySet keys { "one", "two", "ONE", "three" };
    Mappings mappings {
        { "one", 1 }, { "two", 2 }, { "ONE", 1 }, { "three", 3 }
    };

    assert(keys.size() == 3);
    assert(mappings.size() == 3);
}

>

  • 使用C++17,两个断言都传递(编译器资源管理器)。

    切换到C++20,第二个断言失败(编译器资源管理器)

    output.s:./example.cpp:28:int main():Assertion`mappings.size()==3'失败。

    一个明显的解决办法是在C++20模式下有条件地提供运算符<=>:编译资源管理器

    #if defined(__cpp_lib_three_way_comparison)
        std::weak_ordering operator<=>(CiKey const& other) const {
            if (boost::ilexicographical_compare(*this, other)) {
                return std::weak_ordering::less;
            } else if (boost::ilexicographical_compare(other, *this)) {
                return std::weak_ordering::less;
            }
            return std::weak_ordering::equivalent;
        }
    #endif
    

    让我吃惊的是,我遇到了另一个中断更改的情况--在这种情况下C++20在没有诊断的情况下更改了代码的行为。

    在我阅读std::tuple::operator<时,它应该起作用了:

    3-6)通过运算符<LHSRHS进行词典比较,即比较第一个元素(如果它们等价),比较第二个元素(如果它们等价),比较第三个元素,以此类推。对于非空元组,(3)等价于

    if (std::get<0>(lhs) < std::get<0>(rhs)) return true;
    if (std::get<0>(rhs) < std::get<0>(lhs)) return false;
    if (std::get<1>(lhs) < std::get<1>(rhs)) return true;
    if (std::get<1>(rhs) < std::get<1>(lhs)) return false;
    ...
    return std::get<N - 1>(lhs) < std::get<N - 1>(rhs);
    

    我知道从技术上讲,这些从C++20开始就不适用了,取而代之的是:

    通过合成的三元比较(见下文)对LHSRHS进行词典比较,即比较第一个元素(如果它们等价),比较第二个元素(如果它们等价),比较第三个元素,依此类推

    连同

    <、<=、>、>=和!=运算符分别由运算符<=>运算符==合成。(自C++20起)

    问题是,

    >

  • 我的类型没有定义运算符<=>运算符==,

    正如这个答案所指出的,另外提供运算符<会很好,并且应该在计算诸如a 这样的简单表达式时使用。

    1. C++20中的行为更改是否正确/是故意的?
    2. 是否应进行诊断?
    3. 我们可以使用其他工具来发现像这样的无声破坏吗?在元组/中扫描整个代码库以了解用户定义类型的使用情况,感觉并不能很好地缩放。
    4. 除了元组/之外,是否还有其他类型可以显示类似的更改?

  • 共有2个答案

    岳刚洁
    2023-03-14

    啊!@说书人用他们的评论钉死了:

    “My type不定义运算符<=>也不定义运算符==”--但std::string定义了运算符,这使它成为D[e]向基转换的候选项。我相信所有支持比较的标准库类型都对其成员进行了彻底检查。

    实际上,一个更快的解决办法是:

    #if defined(__cpp_lib_three_way_comparison)
        std::weak_ordering operator<=>(
            CiKey const&) const = delete;
    #endif
    

    成功了!编译器资源管理器

    更好的解决方案,正如StoryTeller的第二条评论所暗示的:

    我想非虚拟析构函数不再是避免从标准库容器继承的唯一有力理由:/

    将避免这里的继承:

    // represents case insensiive keys
    struct CiKey {
        std::string _value;
    
        bool operator<(CiKey const& other) const {
            return boost::ilexicographical_compare(_value, other._value);
        }
    };
    

    当然,这需要对使用代码进行(一些)下游更改,但它在概念上更纯净,并且避免了将来这种“标准蠕变”。

    编译器资源管理器

    #include <boost/algorithm/string.hpp>
    #include <iostream>
    #include <set>
    #include <version>
    
    // represents case insensiive keys
    struct CiKey {
        std::string _value;
    
        bool operator<(CiKey const& other) const {
            return boost::ilexicographical_compare(_value, other._value);
        }
    };
    
    using KeySet   = std::set<CiKey>;
    using Mapping  = std::tuple<CiKey, int>;
    using Mappings = std::set<Mapping>;
    
    int main()
    {
        KeySet keys { { "one" }, { "two" }, { "ONE" }, { "three" } };
        Mappings mappings { { { "one" }, 1 }, { { "two" }, 2 }, { { "ONE" }, 1 },
            { { "three" }, 3 } };
    
        assert(keys.size() == 3);
        assert(mappings.size() == 3);
    }
    

    我们怎样才能诊断出这样的问题。它们是如此微妙,以至于可以逃避代码审查。这种情况由于标准C++已经有20年的历史而变得更加严重,在这种情况下,标准C++的工作非常好,而且可以预测。

    我想作为旁白,我们可以预期任何“提升”运算符(考虑std::variant/std::optional)在与过多继承标准库类型的用户定义类型一起使用时都有类似的缺陷。

    宗政博
    2023-03-14

    基本的问题来自这样一个事实:您的类型不连贯,并且标准库直到C++20才调用您。也就是说,你的类型总是有点破碎,但事情的定义足够狭隘,你可以逃脱它。

    您的类型被破坏了,因为它的比较运算符没有意义。它通告它是完全可比的,定义了所有可用的比较运算符。发生这种情况是因为您公开继承了std::string,因此您的类型通过隐式转换到基类来继承那些运算符。但是,这组比较的行为是不正确的,因为您只替换了其中一个比较,而这个比较与其他的比较不一样。

    由于行为是不一致的,一旦C++真的关心你的一致性,可能会发生什么事情就等着你去抓了。

    C++20中的行为改变是正确的/故意的吗?

    那要看你说的“故意”是什么意思了。你的类型总是有这个问题;只是它在C++20中很重要。或者换一种说法,C++20比以前的版本更注重比较的一致性。

    如果您为类型提供了完整的比较运算符,就不会有这个问题。所以从这个角度来看,C++20的行为是好的;是你的类型错了。

    应该有诊断吗?

    问题是您对操作符的实现不一致。没有概念可以检测到这一点。这是你做出的承诺。

    我们能用其他的工具来发现像这样的无声破坏吗?

    解决这一问题的最佳方法是找到实现一组不完整的比较运算符的所有类型,然后完成它们的实现。这在工具中不容易做到,因为它们可以从类型外部定义。

     类似资料:
    • 问题内容: 我们在该网站上有一个庞大的应用程序,并且我们有一些链接,比如我们网站上的蓝色链接就是蓝色。现在,我想建立其他一些链接,但颜色要浅一些。显然,我可以简单地通过在CSS文件中添加十六进制代码来完成此操作,但是我们的站点允许用户确定其自定义配置文件/站点(例如Twitter)所需的颜色。 所以,我的问题是:我们可以按百分比减少颜色吗? 假设以下代码是CSS: 要么 有没有一种方法可以减少一定

    • 本文向大家介绍Python通过`is`与`==`比较,包括了Python通过`is`与`==`比较的使用技巧和注意事项,需要的朋友参考一下 示例 常见的陷阱是混淆相等比较运算符is和==。 a == b比较的值a和b。 a is b将比较认同的a和b。 为了显示: 基本上,is可以视为的简写。id(a) == id(b) 除此之外,还有一些运行时环境的怪癖使事情变得更加复杂。True与相比,短字符

    • 问题内容: 在Python 2.7中,我定义了一个空的新类: 然后创建新类的实例列表: 然后尝试对列表进行排序: 令人惊讶的是,即使我还没有定义一种比较以下实例的方法,该类也不会抱怨: 那里到底发生了 什么?这种行为的理由是什么(可能令人惊讶)? 问题答案: 我认为唯一的理由是可以方便地对对象进行排序,例如将其用作具有某些默认行为的字典键。语言定义中的相关章节位于:https : //docs.p

    • 过滤出数组中比较函数不返回 true 的所有值。 类似于difference ,除了接受一个 comparator (比较函数)。 使用 Array.filter() 和 Array.findIndex() 来查找合适的值。 const differenceWith = (arr, val, comp) => arr.filter(a => val.findIndex(b => comp(a, b

    • 问题内容: 以下是一个简单的声明 当我运行它时输出为。我不知道原因。 在执行期间(字节)-1返回-1,因此等价于。当我打印(char)-1时,它 仅 在某些系统中打印,而不在所有系统中打印。 如果我忽略上述第二点并打印(int)’?’ 然后打印63 所以我的问题是,如果我一起进行多播,那我得到了,但是如果我分部进行了广播,那 (字节)-1 (字符)-1 (int)’?’ 然后我得到63,为什么呢?

    • 问题内容: 当我在Python单元测试中比较两个Unicode字符串时,它会给出一个不错的失败消息,突出显示哪些行和字符不同。但是,比较两个8位字符串只会显示两个字符串而不会突出显示。 如何获得Unicode和8位字符串的突出显示? 这是一个显示两个比较的示例单元测试: 该测试的结果显示出差异: 问题答案: 对Python源代码的一点点挖掘表明,注册了许多方法来测试不同类型的相等性。 您可以看到已