我不明白为什么在下面的代码中表达式C c3 = 5 + c;
尽管可以像前面的语句那样将5转换为C类型,但是无法编译。
#include <iostream>
class C
{
int m_value;
public:
C(int value): m_value(value) {} ;
int get_value() { return m_value; } ;
C operator+(C rhs) { return C(rhs.m_value+m_value); }
};
int main()
{
C c = 10;
C c2 = c + 5; // Works fine. 5 is converted to type C and the operator + is called
C c3 = 5 + c; // Not working: compiler error. Question: Why is 5 not converted to type C??
std::cout << c.get_value() << std::endl; // output 10
std::cout << c2.get_value() << std::endl; // output 15
}
答案 0 :(得分:21)
因为如果overload operator作为该类的成员函数,则只能在将该类的对象用作左操作数时调用它。 (并且左操作数成为要调用的成员函数的隐式*this
对象。)
二进制运算符通常实现为非成员以保持对称性(例如,当添加复数和整数时,如果operator +是复杂类型的成员函数,则仅
complex+integer
会编译,并且不是integer+complex
)。
根据标准,[over.match.oper]/3
(重点是我的)
对于一元运算符@,其操作数的类型为cv-unqualified 版本是T1,对于二进制运算符@,其左操作数为 CV非限定版本为T1且类型为右操作数的类型 其简历不合格版本为T2,四组候选函数, 内置指定成员候选人,非成员候选人 候选者和重写的候选者的构造如下:
- (3.1)如果T1是完整的类类型或当前正在定义的类,则候选成员集是合格的结果 查找T1 :: operator @([over.call.func]);否则, 成员候选人为空。
这意味着,如果左操作数的类型不是类类型,则候选成员的集合为空;重载的运算符(作为成员函数)将不被考虑。
您可以将其作为非成员函数重载,以允许对左操作数和右操作数进行隐式转换。
C operator+(C lhs, C rhs) { return C(lhs.get_value() + rhs.get_value()); }
然后c + 5
或5 + c
都可以正常工作。
BTW:这将导致构造一个临时对象(从int
到C
)以调用非成员函数;如果您对此有所关注,则可以按如下所示添加所有三个可能的重载。还请注意,这是一个权衡的问题。
C operator+(C lhs, C rhs) { return C(lhs.get_value() + rhs.get_value()); }
C operator+(C lhs, int rhs) { return C(lhs.get_value() + rhs); }
C operator+(int lhs, C rhs) { return C(lhs + rhs.get_value()); }
here是有关何时使用普通,朋友或成员函数重载的一些建议。
在大多数情况下,该语言由您决定是否 您要使用普通/朋友或成员函数版本的 超载。但是,通常两者之间的比较好 其他。
在处理不修改左操作数的二进制运算符时 (例如operator +),通常是普通版或好友功能版 首选,因为它适用于所有参数类型(即使 左操作数不是类对象,或者不是 可修改)。普通或朋友功能版本已添加 对称性的好处,因为所有操作数都成为显式参数 (而不是左操作数变为* this和右操作数 成为显式参数)。
在处理确实修改了左操作数的二进制运算符时 (例如operator + =),通常首选成员函数版本。 在这些情况下,最左边的操作数将始终是类类型,并且 使被修改的对象成为* this指向的对象 自然。由于最右边的操作数成为显式参数, 谁在被修改以及谁在被修改没有困惑 评估。
答案 1 :(得分:8)
您正面临将某些运算符重载定义为自由函数的原因,即当需要隐式转换时。要了解幕后情况,请考虑操作符重载调用的详细形式:
C c2 = c.operator+(5); // Ok, c has this member function
C c3 = 5.operator+(c); // No way, this is an integer without members
您显然可以做的是像{p>那样的显式C
构造
C c3 = C{5} + c;
,但不适用于C
之类的算术值类型。为了使隐式构造成为可能,请将重载定义为自由函数
auto operator + (C lhs, const C& rhs)
{
lhs += rhs;
return lhs;
}
现在,左侧操作数没有限制。请注意,运算符是根据+=
实现的(您必须对其进行实现才能编译以上代码),这是this thread中指出的良好做法:当您提供二进制数{{ 1}}(对于自定义类型),该类型的用户还将期望operator +
可用。因此,为了减少代码重复,通常最好用operator +=
来实现+
(与所有其他算术操作数相同)。
还要注意,这些操作数通常需要大量的样板代码。为了减少这种情况,请考虑Boost operators library。要基于最少的实际手写代码生成 all 个标准算术运算符,请执行以下操作:
+=
答案 2 :(得分:3)
这里还有一个补充说明(有点“荒谬的还原”),为什么您关于编译器可以将左手参数隐式转换为C
的建议实际上会打开一堆蠕虫。简单地说,实际的语言规则是,在应用转换之前,需要进行名称查找(用于函数调用和对(用户声明的)运算符的调用),以找到候选集。此时,尚未考虑操作数类型,但是 scope 很好。因此,只要用户声明的运算符的范围是用户声明的运算符,且其声明的第一个参数属于(cv限定的)类类型,则第一个参数的类型确实很重要。找到后,编译器将尝试应用转换规则并对候选对象进行排名等。
(因此,您的问题有点令人误解,因为在您的示例中,我们甚至没有涉及转换逻辑,而是名称解析已经为空。)
现在,想象一下我们可以简单地更改语言,说在名称解析之前也可以转换第一个参数。这里需要一点手工操作,因为这意味着我们必须进行转换,查找名称,然后再次进行转换,因此在实践中如何工作当然还不清楚。无论如何,然后看这个例子:
struct B;
struct A
{
A(int);
A operator +(B) const;
};
struct B
{
B(int);
B operator +(B) const;
};
现在,1 + B{3}
应该做什么?显然,它可以转换为B{1} + B{3}
。但是谁能说我们不能A{1} + B{3}
呢?为什么B
的构造函数比A
的构造函数更受青睐?当然,我们可以争论说B
是首选,因为看看B{...}+B{...}
有多漂亮和对称(好吧,我有点滑稽)。或者,我们可以采取更安全的方法,如果该程序包含此类歧义,则说明该程序格式错误。但是还有很多其他情况需要考虑,例如如果B
的构造函数是explicit
会怎样–编译器(仍然是新编译的)还是出错了,还是应该悄悄地切换到A
的可用隐式转换?
另一个不明显的地方是哪种类型应考虑哪些范围(例如名称空间)?如果您在{.1全局名称空间范围,并且编译器将挖掘出某种类型的operator +
,将其操作数隐式转换为该类型,然后对其执行操作。
还请注意,即使可以合理合理的方式指定此功能,也可能会耗费大量编译时间(实际上,重载分辨率已经是编译C ++时最大的开销之一,也是原因之一为什么编译C ++代码会比编译C花费更长的时间。
TL; DR: