C++ 比较运算符重载常量与非常量行为

Sim*_*onT 5 c++ compiler-errors operator-overloading language-lawyer c++20

我最近注意到一些我自己无法弄清楚的运算符重载行为。以下两个类仅const在 的成员比较运算符重载上有所不同ClassA。在ClassB他们没有const的。一般来说,我知道人们总是更喜欢那个const,但我仍然对为什么我们会看到我将在下面描述的行为感兴趣。

#include <string>

class ClassA {
public:
    explicit ClassA(double t) : _t(t) {}

    std::string operator<=(int const& other) const {
        return "A(<=)";
    }

    std::string operator==(int const& other) const {
        return "A(==)";
    }

    friend std::string operator<=(int const& other, ClassA const& expr) {
        return "A'(<=)";
    }

    friend std::string operator==(int const& other, ClassA const& expr) {
        return "A'(==)";
    }

private:
    double _t;
};

class ClassB {
public:
    explicit ClassB(double t) : _t(t) {}

    std::string operator<=(int const& other) {
        return "B(<=)";
    }

    std::string operator==(int const& other) {
        return "B(==)";
    }

    friend std::string operator<=(int const& other, ClassB const& expr) {
        return "B'(<=)";
    }

    friend std::string operator==(int const& other, ClassB const& expr) {
        return "B'(==)";
    }

private:
    double _t;
};
Run Code Online (Sandbox Code Playgroud)

现在我想在const和非常量场景中使用这些类和比较函数。

int
main(int argc,
     char* argv[]) {
    ClassA a1{0};
    1==a1; //OK
    1<=a1; //OK
    ClassA const a2{0};
    1==a2; //OK
    1<=a2; //OK
    ClassB b1{0};
    1==b1; //NOT OK
    1<=b1; //OK
    ClassB const b2{0};
    1==b2; //OK
    1<=b2; //OK

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

一切正常,但我标记的那一行NOT OK。这会引发编译器错误。

error C2446: '==': no conversion from 'ClassB' to 'int'
Run Code Online (Sandbox Code Playgroud)

我的问题分为三个部分,但我希望有一个很好的理由可以回答所有问题。所以我希望将其发布到一个 SO 问题中仍然可以。

==当不等式编译时,为什么相等运算符不编译<=?为什么成员函数是否存在对友元函数很重要const?为什么制作ClassB对象会const修复它?

更新:

  • 在评论中@Eljay 指出该问题可能是由新的 C++20 特性造成的,该特性会自动生成带有反向参数的比较运算符。这显然使成员std::string operator==(int const& other)(重新安排后)更适合1==b1. 经过一番挖掘,我发现规则说这些应该在重载解析规则中生成。
  1. 改写的候选人:
  • 对于四个关系运算符表达式 x<y、x<=y、x>y 和 x>=y,将找到的所有成员、非成员和内置运算符<=> 添加到集合中。
  • 对于四个关系运算符表达式 x<y、x<=y、x>y 和 x>=y 以及三路比较表达式 x<=>y,两个参数顺序颠倒的合成候选为找到的每个成员、非成员和内置 operator<=> 添加。
  • 对于 x!=y,找到的所有成员、非成员和内置运算符==都被添加到集合中。
  • 对于相等运算符表达式 x==y 和 x!=y,为找到的每个成员、非成员和内置运算符==添加了两个参数顺序相反的综合候选。

在所有情况下,在重写表达式的上下文中都不考虑重写的候选对象。对于所有其他算子,重写的候选集为空。

  • @463035818_is_not_a_number 指出了一些关于可以和不能编译不同版本代码的不同编译器的有趣发现。特别是对于clanggcc-std=c++2a的最新版本,x86-64 clang 12.0.0并且x86-64 gcc 11.1不进行编译,而旧版本x86-64 clang 9.0.1x86-64 gcc 9.4做。对于 VisualStudio,我们看到带有标志的类似模式/std:c++latest。这里最新版本x64 msvc v19.28 (VS16.9)不编译,直接前身编译x64 msvc v19.28。这些测试是使用 Compiler explorer godbolt.org 进行的

  • 值得注意的是,编译器错误clanggcc暗示问题在于std::string operator==(int const& other)没有返回bool.

error: return type 'std::string' (aka 'basic_string<char>') of selected 'operator==' function for rewritten '==' comparison is not 'bool'
    1==b1; //NOT OK
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会

error: return type of 'std::string ClassB::operator==(const int&)' is not 'bool'
    1==b1; //NOT OK
Run Code Online (Sandbox Code Playgroud)

虽然这些都是非常有趣的见解,但最初的问题仍然悬而未决。

raw*_*rex 1

没有具体的答案。不过,我们还是来看看文档吧。

重载运算符的返回类型没有限制(因为返回类型不参与重载决策),但有规范的实现

...该语言对重载运算符的作用或返回类型没有其他限制,但一般来说,重载运算符的行为应尽可能与内置运算符相似

进而:

..返回类型受到预期使用运算符的表达式的限制

例如,赋值运算符通过引用返回,使得可以编写 a = b = c = d,因为内置运算符允许这样做。

我们进一步挖掘:

...其中内置运算符返回 bool,大多数用户定义的重载也返回 bool,以便用户定义的运算符可以以与内置运算符相同的方式使用。但是,在用户定义的运算符重载中,任何类型都可以用作返回类型(包括 void)。

甚至更进一步 (三向比较)

如果两个操作数都具有算术类型,或者一个操作数具有无范围枚举类型而另一个操作数具有整型类型,则对操作数应用通常的算术转换。

因此,我断言这取决于实现。在我的机器上它编译(g++)并运行:

std::cout << (1==b1) << std::endl; // Prints B'(==)
Run Code Online (Sandbox Code Playgroud)

小小重新更新

@463035818_is_not_a_number“这个问题在 VS 中出现。较新版本的 gcc 也拒绝这种用法,与 clang 相同。它看起来像是一个错误/缺少功能,并在较新的版本中得到修复。”

这是存在问题的编译器资源管理器片段。