运算符重载
文章目录
为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 for ‘operator+’ (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
对象与int
和double
值相加。然而,这个运算符不具有互换性。
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;
}
编译器生成的比较运算符
注意:SpreadsheetCell
的operator==
和<=>
的实现,它们只是简单的比较所有的数据成员。在这种情况下,可以进一步的减少需要编写的代码行数,因为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!