c++运算符重载

运算符重载

为SpreadsheetCell实现加法

请看下面代码

class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(int value) : m_val{value} {}
    int getValue() const
    {
        return m_val;
    }

private:
    int m_val;
};

int main()
{
    int a{1}, b{2};
    a + b;
    SpreadsheetCell s1{1}, s2{2};
    s1 + s2;
}

上述代码创建了一个电子表格单元格类,在main()函数内又创建了两个SpreadsheetCell对象,并且将两个对象相加。对于第21行a+b两个基本类型变量相加肯定是可以编译通过的,但是第23行两个对象相加编译就会报错。报错信息如下:

 error: no match foroperator+(operand types are ‘SpreadsheetCell’ and ‘SpreadsheetCell’)

在c++中没有对自定义类型进行运算符'+'运算的实现,所以需要手动实现自定义类型的相加。

1.首次尝试:add方法

增加一个add函数,该函数实现SpreadsheetCell对象相加

class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(int value) : m_val{value} {}
    int getValue() const
    {
        return m_val;
    }
    SpreadsheetCell add(const SpreadsheetCell &cell) const;

private:
    int m_val;
};

SpreadsheetCell SpreadsheetCell::add(const SpreadsheetCell &cell) const
{
    return SpreadsheetCell{getValue() + cell.getValue()};
}

这种方法的确可以实现自定义的SpreadsheetCell类型相加,但是有点笨拙。

2.第二次尝试:将operator+作为方法重载

'+'号对两个单元格相加比较方便,就像对基本类型的相加一样。

c++中允许编写自己的加号版本,以正确的处理类,称为加运算符。为此可以编写一个名为operator+的方法,如下:

class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(int value) : m_val{value} {}
    int getValue() const
    {
        return m_val;
    }
    SpreadsheetCell operator+(const SpreadsheetCell &cell) const;

private:
    int m_val;
};

// 允许在operator和'+'之间存在空格
SpreadsheetCell SpreadsheetCell::operator+(const SpreadsheetCell &cell) const
{
    return SpreadsheetCell{getValue() + cell.getValue()};
}

对于operator+这样的写法可能有点奇怪,不用过多担心,这就是一个名称,就像add函数一样。当c++编译器分析一个程序,遇到运算符(例如:+、-、=或<<)时,就会查找名为operator+operator-operator=或者operator<<,且具有适当参数的函数或者方法。

SpreadsheetCell s1{1}, s2{2};

SpreadsheetCell s3{s1 + s2};
// 等同于
SpreadsheetCell s3{s1.operator+(s2)};

注意,用作operator+参数的对象类型并不一定要与编写operator+的类相同。同时可以指定operator+的返回类型。

隐式转换
class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(double value) : m_val{value} {}
    SpreadsheetCell(string_view value)
        : SpreadsheetCell{stringToDouble(value)} {}

    double getValue() const
    {
        return m_val;
    }

    SpreadsheetCell operator+(const SpreadsheetCell &cell) const;

    string doubleToString(double value) const;
    double stringToDouble(string_view value) const;

private:
    double m_val{0};
};
string SpreadsheetCell::doubleToString(double value) const
{
    return to_string(value);
}
double SpreadsheetCell::stringToDouble(string_view value) const
{
    double number{0};
    from_chars(value.data(), value.data() + value.size(), number);
    return number;
}

// 允许在operator和'+'之间存在空格
SpreadsheetCell SpreadsheetCell::operator+(const SpreadsheetCell &cell) const
{
    return SpreadsheetCell{getValue() + cell.getValue()};
}

int main()
{
    SpreadsheetCell s1{1}, s4{0}, s5{0};
    string str{"124"};
    s4 = s1 + string_view(str);
    cout << "s4.value = " << s4.getValue() << endl;
    s5 = s1 + 5.6;
    cout << "s5.value = " << s5.getValue() << endl;
    return 0;
}

执行结果:

s3.value = 3
s4.value = 125
s5.value = 6.6

上述代码中,涉及到数值装换函数from_chars,不会的可以点击此处学习

当编译器看到SpreadsheetCell试图与double类型相加时,发现了用double值作为参数的SpreadsheetCell构造函数,就会生成一个临时的SpreadsheetCell对象,传递给operator+。与此类似,string_view也是同样操作。

由于必须创建临时对象,隐式使用构造函数的效率不高。为避免与double值相加时隐式的使用构造函数,可以编写第二个operator+。如下:

SpreadsheetCell SpreadsheetCell::operator+(const double rhs) const
{
    return SpreadsheetCell{getValue() + rhs};
}

第三次尝试:全局operator+

隐式转换允许使用operator+方法将SpreadsheetCell对象与intdouble值相加。然而,这个运算符不具有互换性。

s4 = s1 + 5;    
s5 = s1 + 5.6;

s4 = 5 + s1;    // error
s5 = 5.6 + s1;  // error

当对象在左边时,隐式装换正常运行,但是在右边就无法运行。不符合加法的运算规律。问题在于必须在SpreadsheetCell对象上调用operator+方法,对象必须在operator+的左边。这是c++语言定义的方式,因此,使用operator+方法无法让上面的代码运行。

然而,如果用不局限于某个特定对象的全局operator+函数替换类内的operator+方法,上面的代码即可运行。示例:

SpreadsheetCell operator+(const SpreadsheetCell &lhs, const SpreadsheetCell &rhs)
{
    return SpreadsheetCell{lhs.getValue() + rhs.getValue()};
}

假如我编写出下面这样的代码,编译器会如何应对呢?

SpreadsheetCell s1;
s1 = 1.1 + 5.5;

首先,这个代码是肯定可以运行的,但是并没有调用前面operator+。这段代码将普通的double型数值1.1与5.5相加,得到了下面展示的中间语句:

s1 = 6.6;

为了让赋值操作继续,运算符右边应该是SpreadsheetCell对象。编译器找到非explicit(防止隐式转换)的由用户定义的double值作为参数的构造函数,然后将这个构造函数隐式的将double值转换为一个临时SpreadsheetCell对象,最后调用赋值运算符。

注意:在c++中不能更改运算符的优先级,也不能发明新的运算符号,不允许更改运算符的实参个数。

重载算数运算符

上述已经对自定义类型实现了+的操作,所以对减法、乘法、除法都类似的操作。还可以重载%。

对于operator/而言,唯一棘手之处是记着检查除数是否为0。如果检测到除数为0,该实现将抛出异常。

SpreadsheetCell operator/(const SpreadsheetCell &lhs, const SpreadsheetCell &rhs)
{
    if (rhs.getValue() == 0)
    {
        throw invalid_argument{"Divide by zero"};
    }
    return SpreadsheetCell{lhs.getValue() / rhs.getValue()};
}

c++并没有真正要求在operator*中实现乘法,在operator/中实现除法。可在operator/中实现乘法,在operator+中实现除法,以此类推。然而这样做会让人非常迷惑,也没有理由这么去做。在实现中应该尽量使用常用的运算符含义。

除基本算术运算符外,C++还提供了简写运算符,例如+=和-=。这些运算符与基本算数运算符不同,它们会改变运算符左边的对象,而不是创建一个新的对象。此外还有一个微妙的差别,它们生成的结果是对被修改对象的引用,这一点与赋值运算符类似。

简写算数运算符的左边总要有一个对象,因此应该将其作为方法,而不是全局函数。

class SpreadsheetCell
{
public:
    SpreadsheetCell &operator+=(const SpreadsheetCell &rhs);
    SpreadsheetCell &operator-=(const SpreadsheetCell &rhs);
    SpreadsheetCell &operator*=(const SpreadsheetCell &rhs);
    SpreadsheetCell &operator/=(const SpreadsheetCell &rhs);

    double getValue() const
    {
        return m_val;
    }
private:
    double m_val{0};
};

SpreadsheetCell &SpreadsheetCell::operator+=(const SpreadsheetCell &rhs)
{
    this->m_val += rhs.getValue();
    return *this;
}

// -=、*=、/=类似操作。

如果代码中既有某个运算符的普通版本,又有简写版本(如+=),建议基于简写版本实现普通版本,以避免代码重复。例如:

SpreadsheetCell operator+(const SpreadsheetCell &lhs, const SpreadsheetCell &rhs)
{
    auto result{lhs};
    result += rhs; // 调用+=版本;
    return result;
}

重载比较运算符

比较运算符(例如>、<、<=、>=、==和!=)是另一组对类有用的运算符。c++20标准为这些操作带来了很多变化,并添加了三向比较运算符,也成为宇宙飞船运算符,<=>

与基本算数运算符一样,6个c++20之前的比较运算符应该是全局函数,这样就可以在运算符的左侧或者右侧参数上使用隐式转换。比较运算符都返回bool类型,当然,也可以更改返回类型,但是不建议这样做。

下面是比较运算符的声明,必须用>、<、<=、>=、==和!=替换< op >,得到6个函数

bool operator<op>(const SpreadsheetCell &lhs, const SpreadsheetCell &rhs);

下面是函数operator==的定义,其他类似:

bool operator==(const SpreadsheetCell &lhs, const SpreadsheetCell &rhs)
{
	return (lhs.getValue() == rhs.getValue());
}

注意:大多数时候,最好不要对浮点数执行相等或者不相等的操作,应该用ε测试(epsilon test)。

当类中的数据成员较多时,比较每个数据成员可能比较痛苦。然而当实现了==<之后,可以根据这两个运算符编写其他比较运算符。例如,下面的operator>=定义中使用了operator<

bool operator>=(const SpreadsheetCell &lhs, const SpreadsheetCell &rhs)
{
	return !(lhs < rhs);
}

如果你需要支持所有的比较运算符,需要写很多的代码。

在c++20中,简化了向类中添加对比较运算符的支持,首先,对于c++20,实际上建议将operator==实现为类的成员函数,而不是全局函数,还要注意,添加[[nodiscard]]属性是一个不错的操作,这样操作符的结果就不能被忽略。

在c++20中,单个的operator==重载就可以使下面的代码生效

if (myCell == 10) {cout << "myCell == 10" << endl;}
if (10 == myCell) {cout << "10 == myCell" << endl;}

上面的10 == myCell的表达式由c++20编译器重写为myCell == 10,可以为其调用operator==成员函数。此外, 通过实现operator==,c++20会自动添加对!=的支持。

接下来,要实现对全套的比较运算符的支持,在c++20中,只需要实现一个额外的重载运算符,operator<=>,一旦类有了运算符==<=>的重载,c++20就会自动为所有的6个比较运算符提供支持,对于spreadsheetCell类,运算符<=> 如下:

[[nodiscard]] std::partial_ordering operator<=>(const SpreadsheetCell& rhs) const;

注意:c++20编译器不会根据<=>重写或者!=,这样做是为了避免性能问题,因为operator的显示实现通常比<=>更加高效!

重载返回的类型partial_ordering是c++20引入的,是三向比较的结果类型。

<=>实现如下:

partial_ordering SpreadsheetCell::operator<=>(const SpreadsheetCell &rhs) const
{
    return getValue() <=> rhs.getValue();
}

注意编译的时候,如果你的编译器不支持c++20,要加上-std=c++20的选项。

如果用SpreadsheetCell类与double类型比较,则会和前面一样,会产生隐式转换。如果希望避免隐式转换带来对性能上的影响,可以为double提供特定的重载。对于现在,有了c++20,不需要有太多的工作,只需要提供两个额外的重载运算符作为方法。

[[nodiscard]] bool operator==(double rhs) const;
[[nodiscard]] std::partial_ordering operator<=>(double rhs) const;

实现如下:

bool SpreadsheetCell::operator==(double rhs) const
{
	return getValue() == rhs;
}

std::partial_ordering SpreadsheetCell::operator<=>(double rhs) const
{
	return getValue() <=> rhs;
}

编译器生成的比较运算符

注意:SpreadsheetCelloperator==<=>的实现,它们只是简单的比较所有的数据成员。在这种情况下,可以进一步的减少需要编写的代码行数,因为c++20可以为我们编写这些代码。例如,拷贝构造函数可以显示的设置为默认。operator==<=>也可以默认。在这种情况下,编译器会为你编写他们,并通过依次比较每个数据成员来实现他们。此外,如果只是显示的使用默认operator<=>,编译器会自动包含一个默认的operator==。因此,对于SpreadsheetCell类,如果没有显示的double版本的operator==<=>,只需要编写以下单行代码,即可添加对所有的6个比较运算符的完全支持。

[[nodiscard]]std::partial_ordering operator<=>(const SpreadsheetCell&)const = default;

如果用户显示的添加了double版本的<=>,则编译器不在自动生成operator==(const SpreadsheetCell &),因此必须将其显示默认,如下所示:

class SpreadsheetCell {
public:
    [[nodiscard]] auto operator<=>(const SpreadsheetCell &rhs) const = default;
    [[nodiscard]] bool operator==(const SpreadsheetCell &rhs) const = default;

    [[nodiscard]] std::partial_ordering operator<=>(double rhs) const;
    [[nodiscard]] bool operator==(double rhs) const;
};

建议将显示的将operator<=>设置为默认,通过让编译器为你编写,它将与新添加或者修改的数据成员保持同步。

注意:只有当operator==<=>使用定义操作的类类型的const引用作为参数的时候,才可能显示的将operator==<=>设置为默认。例如下列的操作将不起作用:

[[nodiscard]] auto operator<=>(double) const = default;  // dont work!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值