通过C ++ 20三向比较,可以实现更安静的行为更改 [英] More silent behaviour changes with C++20 three-way comparison

查看:99
本文介绍了通过C ++ 20三向比较,可以实现更安静的行为更改的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

To my surprise, I ran into another snag like C++20 behaviour breaking existing code with equality operator?.

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

Consider a simple case-insensitive key type, to be used with, e.g., std::set or std::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,第二个断言失败(编译器资源管理器)

    Switching to C++20, the second assert fails (Compiler Explorer)

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

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

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

    An obvious work-around is to conditionally supply operator<=> in C++20 mode: Compile Explorer

    #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在不进行诊断的情况下更改了代码的行为.

    Question

    It surprises me that I ran into another case of breaking changes - where C++20 changes behaviour of code without diagnostic.

    在我的阅读 std :: tuple :: operator< 它应该已经起作用:

    On my reading of std::tuple::operator< it should have worked:

    3-6)通过 operator< 在字典上比较 lhs rhs ,即,比较第一个元素(如果它们相等),比较第二个元素(如果它们是等效的)比较第三个元素,等等.对于非空元组,(3)等效于

    3-6) Compares lhs and rhs lexicographically by operator<, that is, compares the first elements, if they are equivalent, compares the second elements, if those are equivalent, compares the third elements, and so on. For non-empty tuples, (3) is equivalent to

    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开始,从技术上讲这些都不适用,它被替换为:

    I understand that technically these don't apply since C++20, and it gets replaced by:

    通过合成三向词典顺序比较 lhs rhs 比较(请参见下文),即比较前几个元素(如果有)相等,比较第二个元素(如果相等),比较第三个元素,依此类推

    Compares lhs and rhs lexicographically by synthesized three-way comparison (see below), that is, compares the first elements, if they are equivalent, compares the second elements, if those are equivalent, compares the third elements, and so on

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

    问题是,

    • 我的类型未定义 operator< => operator ==

    并作为此答案指出另外提供 operator< 也可以,并且在评估诸如 a<b .

    and as this answer points out providing operator< in addition would be fine and should be used when evaluating simple expressions like a < b.

    1. C ++ 20中的行为更改是否正确/有目的?
    2. 是否应该进行诊断?
    3. 我们可以使用其他工具来发现这种无声破损吗?感觉就像扫描整个代码库以查找 tuple / pair 中用户定义类型的用法一样.
    4. 除了 tuple / pair 之外,还有其他类型可以表现出相似的变化吗?
    1. Is the behavior change in C++20 correct/on purpose?
    2. Should there be a diagnostic?
    3. Can we use other tools to spot silent breakage like this? It feels like scanning entire code-bases for usage of user-defined types in tuple/pair doesn't scale well.
    4. Are there other types, beside tuple/pair that could manifest similar changes?

    推荐答案

    基本问题来自以下事实:您的类型不连贯,并且标准库直到C ++ 20才对您进行调用.也就是说,您的类型总是有点破损,但是定义的范围很窄,您可以摆脱它.

    The basic problem comes from the facts that your type is incoherent and the standard library didn't call you on it until C++20. That is, your type was always kind of broken, but things were narrowly enough defined that you could get away with it.

    您的类型已损坏,因为其比较运算符没有意义.它广告,它具有完全可比性,并定义了所有可用的比较运算符.发生这种情况是因为您公开地继承了 std :: string ,所以您的类型通过隐式转换为基类来继承这些运算符.但是,这种比较方式的行为是不正确的,因为您只用其中一个替换了一个替换项,而该替换项却不如其他替换项.

    Your type is broken because its comparison operators make no sense. It advertises that it is fully comparable, with all of the available comparison operators defined. This happens because you publicly inherited from std::string, so your type inherits those operators by implicit conversion to the base class. But the behavior of this slate of comparisons is incorrect because you replaced only one of them with a comparison that doesn't work like the rest.

    并且由于行为不一致,一旦C ++真正关心您是否保持一致,可能发生的事情就变成了抢劫.

    And since the behavior is inconsistent, what could happen is up for grabs once C++ actually cares about you being consistent.

    但是,更大的问题是标准对待 operator< => 的方式不一致.

    A larger problem however is an inconsistency with how the standard treats operator<=>.

    C ++语言被设计为在采用综合运算符之前,优先考虑显式定义的比较运算符.因此,如果直接比较它们,则从 std :: string 继承的类型将使用您的 operator< .

    The C++ language is designed to give priority to explicitly defined comparison operators before employing synthesized operators. So your type inherited from std::string will use your operator< if you compare them directly.

    但是,C ++库有时会变得更聪明.

    C++ the library however sometimes tries to be clever.

    某些类型尝试转发给定类型提供的运算符,例如 optional< T> .它的设计可比性与 T 相同,并且在此方面很成功.

    Some types attempt to forward the operators provided by a given type, like optional<T>. It is designed to behave identically to T in its comparability, and it succeeds at this.

    但是, pair tuple 会变得更加聪明.在C ++ 17中,这些类型从不实际上转发比较行为.而是根据类型上现有的 operator< operator == 定义综合比较行为.

    However, pair and tuple try to be a bit clever. In C++17, these types never actually forwarded comparison behavior; instead, it synthesized comparison behavior based on existing operator< and operator== definitions on the types.

    因此,他们的C ++ 20版本继承了综合比较的优良传统也就不足为奇了.当然,由于语言进入了该游戏,因此C ++ 20版本决定最好遵循他们的规则.

    So it's no surprise that their C++20 incarnations continue that fine tradition of synthesizing comparisons. Of course, since the language got in on that game, the C++20 versions decided that it was best to just follow their rules.

    除了...它无法完全跟随他们.无法检测到< 比较是合成的还是用户提供的.因此,无法以这些类型之一来实现语言行为.但是,您可以检测到三向比较行为的存在.

    Except... it couldn't follow them exactly. There's no way to detect whether a < comparison is synthesized or user-provided. So there's no way to implement the language behavior in one of these types. However, you can detect the presence of three-way comparison behavior.

    因此,他们做出一个假设:如果您的类型是三向可比的,则您的类型依赖于综合运算符(如果不是,则使用旧方法的改进形式).这是正确的假设;毕竟,由于< => 是一项新功能,所以旧类型不可能获得一个.

    So they make an assumption: if your type is three-way comparable, then your type is relying on synthesized operators (if it isn't, it uses an improved form of the old method). Which is the right assumption; after all, since <=> is a new feature, old types can't possibly get one.

    当然,除非旧类型继承自具有三向可比性的新类型.而且,任何一种类型都无法检测到这种情况.它要么是三元可比的,要么不是三元可比的.

    Unless of course an old type inherits from a new type that gained three-way comparability. And there's no way for a type to detect that either; it either is three-way comparable or it isn't.

    现在,幸运的是,如果您的类型为 doesn',则 pair tuple 的综合三向比较运算符完全能够模仿C ++ 17行为.t 提供了三向比较功能.因此,您可以通过删除 operator< => 重载来显式取消继承C ++ 20中的三向比较运算符,从而恢复原来的行为.

    Now fortunately, the synthesized three-way comparison operators of pair and tuple are perfectly capable of mimicking the C++17 behavior if your type doesn't offer three-way comparison functionality. So you can get back the old behavior by explicitly dis-inheriting the three-way comparison operator in C++20 by deleting the operator<=> overload.

    或者,您可以使用私有继承,而只需使用所需的特定API公开.

    Alternatively, you could use private inheritance and simply publicly using the specific APIs you wanted.

    c ++ 20中的行为更改是否正确/故意?

    Is the behavior change in c++20 correct/on purpose?

    这取决于您故意"的意思.

    That depends on what you mean by "on purpose".

    公开继承自 std :: string 之类的类型在道德上一直有些怀疑.并不是因为切片/析构函数的问题,而是因为它有点作弊.继承此类类型可以直接打开您所不希望的API更改,并且可能不适合您的类型.

    Publicly inheriting from types like std::string has always been somewhat morally dubious. Not so much because of the slicing/destructor problem, but more because it is kind of a cheat. Inheriting such types directly opens you up to changes in the API that you didn't expect and may not be appropriate for your type.

    pair tuple 的新比较版本正在尽其所能,并且在C ++允许的范围内尽其所能.只是您的类型继承了不需要的内容.如果您是从 std :: string 私下继承的,并且仅使用公开了所需的功能,那么您的类型就可以了.

    The new comparison version of pair and tuple are doing their jobs and doing them as best as C++ can permit. It's just that your type inherited something it didn't want. If you had privately inherited from std::string and only using-exposed the functionality you wanted, your type would likely be fine.

    是否应该进行诊断?

    Should there be a diagnostic?

    在某些本机编译器之外无法诊断出这种情况.

    This can't be diagnosed outside of some compiler-intrinsic.

    我们可以使用其他工具来发现这种无声破损吗?

    Can we use other tools to spot silent breakage like this?

    搜索从标准库类型公开继承的情况.

    Search for case where you're publicly inheriting from standard library types.

    这篇关于通过C ++ 20三向比较,可以实现更安静的行为更改的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆