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

Sou*_*mar 16 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

}



Run Code Online (Sandbox Code Playgroud)

son*_*yao 21

因为如果重载运算符作为类的成员函数,则只能在该类的对象用作左操作数时调用它.(并且左操作数成为*this要调用的成员函数的隐式对象.)

二元运算符通常实现为非成员以保持对称(例如,当添加复数和整数时,如果operator +是复杂类型的成员函数,则只complex+integer编译,而不是integer+complex).

从标准,[over.match.oper]/3

(强调我的)

对于具有cv非限定版本为T1的类型的操作数的一元运算符@,以及具有cv非限定版本为T1的类型的左操作数的二元运算符@和具有cv不合格的类型的右操作数版本为T2,四组候选函数,指定成员候选人,非成员候选人,内置候选人和重写候选人,构造如下:

  • (3.1)如果T1是完整的类类型或当前正在定义的类,则成员候选集合是T1 :: operator @([over.call.func])的限定查找的结果; 否则,成员候选人是空的.

这意味着如果左操作数的类型不是类类型,则成员候选集合为空; 不会考虑重载的运算符(作为成员函数).

您可以将其作为非成员函数重载,以允许对左右操作数进行隐式转换.

C operator+(C lhs, C rhs) { return C(lhs.get_value() + rhs.get_value()); }
Run Code Online (Sandbox Code Playgroud)

然后两者c + 55 + c将工作正常.

生活

顺便说一句:这将导致被构造一个temporaray对象(从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()); }
Run Code Online (Sandbox Code Playgroud)

在这里 "再约一些建议时使用正常的,朋友或成员函数重载.

在大多数情况下,语言会由您决定是否要使用重载的普通/好友或成员函数版本.但是,两者中的一个通常是比另一个更好的选择.

处理不修改左操作数的二元运算符(例如operator +)时,通常首选普通函数或友元函数版本,因为它适用于所有参数类型(即使左操作数不是类对象,或者是一个不可修改的类).普通或友元函数版本具有"对称性"的额外好处,因为所有操作数都变为显式参数(而不是左操作数变为*this且右操作数变为显式参数).

当处理修改左操作数的二元运算符(例如operator + =)时,通常首选成员函数版本.在这些情况下,最左边的操作数将始终是类类型,并且正在修改的对象成为*指向的对象,这很自然.因为最右边的操作数变成了一个显式参数,所以不会混淆谁被修改以及谁被评估.


lub*_*bgr 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
Run Code Online (Sandbox Code Playgroud)

你显然可以做一个明确的C结构,如

C c3 = C{5} + c;
Run Code Online (Sandbox Code Playgroud)

但这不适用于算术值类型C.要使隐式构造成为可能,请将重载定义为自由函数

auto operator + (C lhs, const C& rhs)
{
    lhs += rhs;
    return lhs;
}
Run Code Online (Sandbox Code Playgroud)

现在,左侧操作数没有限制.请注意,运算符是根据+=(您必须实现它以使上述代码编译)来实现的,这是本线程中指出的良好实践:当您为operator +自定义类型提供二进制文件时,该类型的用户将预计operator +=也将有空.因此,为了减少代码的重复,它通常是很好的实施+来讲+=(同为其它算术操作数).

进一步注意,这些操作数通常需要大量的样板代码.要减少这种情况,请考虑例如Boost运算符库.要根据最少量的实际手写代码生成所有标准算术运算符:

#include <boost/operators.hpp>

class C : private boost::arithmetic<C>
//                ^^^^^^^^^^^^^^^^^^^^
//                where the magic happens (Barton-Nackmann trick)
{
   int m_value ;

   public:
     C(int value): m_value(value) {} ;

     C& operator+=(const C& rhs) { m_value += rhs.m_value; return *this; }
     C& operator-=(const C& rhs) { m_value -= rhs.m_value; return *this; }
     C& operator*=(const C& rhs) { m_value *= rhs.m_value; return *this; }
     C& operator/=(const C& rhs) { m_value /= rhs.m_value; return *this; }
     const C& operator+() const { return *this; }
     C operator-() const { return {-m_value}; }

     int get_value() { return m_value; } ;
};
Run Code Online (Sandbox Code Playgroud)

  • @Aconcagua如果你用一个临时的左手边调用操作符,你可能会得到一个不必要的副本,对吗? (3认同)

Arn*_*gel 5

这是关于为什么您建议编译器可以将左手参数隐式转换为 a 的额外评论(有点“简化广告荒谬”)C,本质上,这会打开一堆蠕虫。实际的语言规则说,简单地说,在应用转换之前,名称查找 - 用于函数调用和对(用户声明的)运算符的调用 - 以找到候选集。在这一点上,还没有考虑操作数类型,但范围很好。因此,第一个参数的类型 就用户声明的运算符而言,仅当其第一个参数是声明它的(cv 限定的)类类型时才在范围内。当找到候选集时,编译器然后尝试应用转换规则和排名候选人等

(因此,您的问题有点误导,因为在您的示例中,我们甚至没有涉及转换逻辑,而是名称解析已经空了。)

现在,想象一下我们可以简单地改变语言,说第一个参数也可以在名称解析之前被转换。这里需要一点手动操作,因为这意味着我们必须进行转换,查找名称,然后再次进行转换,因此在实践中这将如何工作当然不清楚。无论如何,看看这个例子然后:

struct B;
struct A
{
    A(int);
    A operator +(B) const;
};
struct B
{
    B(int);
    B operator +(B) const;
};
Run Code Online (Sandbox Code Playgroud)

现在,应该1 + B{3}怎么办?显然,它可以转换为B{1} + B{3}. 但谁能说我们不能做A{1} + B{3}呢?为什么B's 的构造函数比A's 更受欢迎?当然,我们可以争辩说,两者B都应该是首选,因为,看看它有多漂亮和对称B{...}+B{...}(好吧,我有点开玩笑)。或者我们可以采取更安全的方式,如果它包含这样的歧义,就说程序是格式错误的。但是还有更多的极端情况需要考虑,例如,如果B构造了 if的构造函数会怎样explicit——编译器(仍然或新的)应该出错,还是应该悄悄地切换到可用的隐式转换A

另一个不明显的问题是应该考虑哪些类型的范围(例如命名空间)?如果您operator +在例如全局命名空间范围内使用,那肯定会令人惊讶,并且编译器会挖掘出某种类型__gnucxx::__internal::__cogwheels::__do_something_impl,将操作数隐式转换为它,然后对其执行操作。

另请注意,即使可以以合理和干净的方式指定此功能,也可能会产生相当大的编译时间成本(实际上,重载解析已经是编译 C++ 时最大的成本之一,也是编译 C++ 的原因之一代码可能比编译 C 花费更长的时间)。

特尔;博士:

  • 有一些棘手的角落案例。
  • 好处是微乎其微的(为什么不像其他人指出的那样使这些运算符成为免费功能)?
  • 关于如何标准化这一点的讨论肯定会很长。