转换逻辑的目标是什么类型?

时间:2019-02-13 09:26:29

标签: c++ operator-overloading

我不明白为什么在下面的代码中表达式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

}



3 个答案:

答案 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 + 55 + c都可以正常工作。

LIVE

BTW:这将导致构造一个临时对象(从intC)以调用非成员函数;如果您对此有所关注,则可以按如下所示添加所有三个可能的重载。还请注意,这是一个权衡的问题。

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:

  • 有些棘手的情况。
  • 好处是微不足道的(为什么不像其他人所指出的那样使这些运算符具有自由功能)?
  • 关于如何对此进行标准化的讨论肯定会很长。