在我教授 C++ 的经验中,运算符重载是最让学生感到悲伤的话题之一。甚至在 stackoverflow 上查看问题:例如,将 + 运算符设为外部还是成员?怎么处理对称性等等,好像挺麻烦的。
当我从 C++ 迁移到 Java 时,我担心我会错过这种能力,但是除了 [] 或 () 之类的运算符之外,我真的从来没有觉得需要重载运算符。事实上,我觉得没有它们的程序更具可读性。
注意:我把它作为一个社区维基。让我们讨论一下。我想听听意见。
在我教授 C++ 的经验中,运算符重载是最让学生感到悲伤的话题之一。甚至在 stackoverflow 上查看问题:例如,将 + 运算符设为外部还是成员?怎么处理对称性等等,好像挺麻烦的。
当我从 C++ 迁移到 Java 时,我担心我会错过这种能力,但是除了 [] 或 () 之类的运算符之外,我真的从来没有觉得需要重载运算符。事实上,我觉得没有它们的程序更具可读性。
注意:我把它作为一个社区维基。让我们讨论一下。我想听听意见。
重载的运算符就像香料。一点点可以让事情变得更好;太多会使它变得难吃。
每个 C++ 程序员都应该知道的一些重载示例,即使他们不赞成:
抱怨重载的运算符很容易,只要它们不以令人惊讶的方式行事,我真的看不出问题。是的,那里有不好的例子(即使在 stl 中)。例如,参见 auto_ptr 的赋值运算符。重载一些运算符,例如&&
和||
几乎,
总是会很糟糕。但在大多数情况下,让运营商做他们宣传的事情并没有真正的问题。
重载operator+
来做一些奇怪的事情是不好的做法,但是如果你将一个名为“Add”的方法放到你的类中,将对象序列化到磁盘,那就同样糟糕了。
重载的运算符可能是做某些事情的好方法,但很容易被滥用。
重载<<
and>>
运算符可以很容易地扩展 C++ 的流,包括新类型的流、新的 I/O 对象以及两者。重载->
使智能指针几乎可以替代 C++ 指针。重载的运算符可以使用字符串连接运算符,并可以构建在语法上类似于int
s 的新类型的数字。拥有它们可以在库中做需要在其他语言中进行语言级别更改的事情。
它们确实有其局限性。没有适合求幂的运算符。只有一个乘法运算符,在某些情况下有不止一种乘法方法(例如,对于 3D 向量,至少有点积和叉积)。、&&
和||
逗号运算符不能复制它们的内置功能,因为它们不能有短路评估和序列点。
当然,它们也可能被滥用。例如,没有语言要求算术运算符必须像算术一样工作。我已经看到为了提出一个有人认为直观的 SQL 表示法而做的可怕的事情。在一个写得很糟糕的 C++ 程序中,不可能知道,比如说,做了什么a = x * y;
,因为它是a.operator=(x.operator*(y));
,或者可能a.operator=(operator*(x, y));
是什么,并且可以编写操作符函数来做任何事情。
Bjarne Stroustrup 设计 C++ 的意图是包含有用的特性而不考虑滥用的可能性,而 James Gosling 设计 Java 的意图是排除过度滥用的特性,即使它们有些用处。我不清楚这些哲学中的任何一个是正确的还是错误的,但它们是不同的。
Java 旨在避免通常需要某些 C++ 特性的情况,例如运算符重载、多重继承和运行时类型推导,因此它们不会经常被遗漏。这到底是好是坏,我不知道。
至于教学生,告诉他们不要自己重载运算符(除了在定义的条件下,例如函子和赋值运算符),而是指出库如何使用重载运算符。我不相信任何 C++ 学生都能正确地完成它们,如果他们能够做到,他们可以并且会自己学习。他们会知道这很棘手,因为你在课堂上禁止这样做。一些我永远不会相信比for
语句更复杂的东西会发现如何重载运算符,并且无论如何都会这样做,但这就是生活。
运算符重载对于很多目的来说都是必不可少的。如果没有重载 operator() 的能力,就不可能创建仿函数。在许多情况下,泛型编程会让人头疼。如果我编写一个数值算法,我依赖于行为相同的值类型,无论它是 float、double、std::complex 还是一些自制类型。我依赖于定义的常用算术运算符等,因此我不必为内置类型编写单独的重载,而为自定义类型编写另一个重载。
智能指针依赖于能够重载解引用运算符的对象,以便它们可以像指针一样工作。
运算符重载对于使 c++ 编程可以接受是极其重要的。至于它很复杂,我只是没有看到。它并不比创建自己的函数更复杂,人们通常觉得这很容易。如果你把它命名为“multiply”,它就是一个函数,如果你把它命名为“operator*”,它就是一个运算符。但是正文中的代码是完全相同的。
当然,运营商有时会被滥用。<< 或 >> 可能是可接受的边界,但它们是如此普遍和使用,以至于我认为这是公平的。
但是,如果您在 C# 中询问过运算符重载,我很乐意不使用它们。它们的实现要笨拙得多,它们不能使用泛型,并且它们不能启用 C++ 使用的所有漂亮和方便的技巧。
我认为这真的取决于用例。有几种类型的类需要运算符。例如,如果没有 -> 和 * 运算符,智能指针将毫无价值。
我还发现比较、相等和赋值运算符对于特定类型非常有用。我在编辑器环境中工作,因此我们自然有多次表示坐标和跨度。当然我们可以用比较运算符做所有事情,但是
if ( point1 > point2 ) ...
只是看起来比负载好
if ( point1.Compare(point2) < 0 ) ...
尽管 cast 偶尔会派上用场,但我发现其他运算符的用处较少。
我不认为运算符重载是一个坏主意。我确实认为将隐式转换作为默认行为是一个坏主意。与运算符重载相结合的默认隐式转换是一个非常糟糕的主意。
完全取消隐式转换 - 或使其依赖于“隐式”关键字 - 该语言永远不会像本文这样的无数文章中讨论的潜在陷阱和陷阱的数量。
正如 Neil 在他的回答中指出的那样,运算符重载是学习良好的面向对象 C++ 习语的必要主题。我会谨慎地向学生教授它,如果你不按照惯用约定实现重载运算符,它可能会导致非常错误和意外的行为。运算符重载不是发挥创造力的好时机。
我使用它们时的运算符和案例:
operator->、operator* - 用于代理对象和不同的包装器。
operator= - 需要避免复制时出现意外行为。
operator < (>, <=, >=) - 用于存储在 map 或 set 中(但通常最好将 functor 传递给这个)。
运算符 << ( >> ) - 用于流和 boost::lexical_cast 兼容性。
运算符 ==, != - 允许比较对象。
操作员 !- 有时改为valid() 函数。
运算符类型 - 用于转换为其他类型。
operator() - 用于智能仿函数,当 boost 被禁止时。
就这样。
有时以前我使用过其他运算符,但那是为了我的数学实用程序。
还应注意逻辑运算符(&&,||) - 我们将与标准语义有所不同:
ptr && ptr->method()
如果我们重载了运算符&&,可能会有其他意义。
我真的很喜欢在 C++ 中为非内置类型重载算术运算符的能力。但仅适用于具有类似算术行为的类型;例如定点类、3D 矢量类、复数类、任意长度的“bignum”类。我用 Java 写过类似的代码,并且因为不得不写类似的东西a.Add(b)
而不是a+b
. 请注意,我是一名受过训练的数学家;在我看来,运算符重载可以让您在 C++ 中获得一些特定于领域的语言优势,而无需实际实现。
但是当我看到例如operator+
功能超载时,我真的很恼火,这些功能最好通过operator<<
(遵循狡猾但完善的 iostream 约定)或类似 STL.push_back()
的模式来完成。
至于operator()
...发现后boost::bind
,boost::function
我无法想象没有函子的生活。如果没有重载等operator*
,智能指针就不会那么方便了。operator->
重载运算符的问题在于,有些人喜欢使用与运算符的原始 (C) 目的没有任何意义的功能来重载它们(这里我指的是 std 中的 >> 和 << 运算符::iostream 的东西。)。在我看来,你应该重载运算符的唯一时间是重载与底层运算符的含义完全匹配(即 < 和 > 必须是比较。)或者,你必须以某种方式重载它以便与另一个交互图书馆。
坦率地说,除非我需要一个库,否则我根本不会重载运算符。它只是让读者工作太努力,无法弄清楚发生了什么。
最好避免重载的运算符之一是转换运算符。它导致了意想不到的结果,甚至 STL 也不喜欢重载它,而是更喜欢函数样式转换:
std::string str = "foo";
char *ch = str.c_str(); //rather than char *ch = str.operator *char();
我想念 Java 中的重载运算符,尤其是在以下情况下:
作为算法或函子的类:(策略模式、责任链、解释器等)。自然是重载 op(); 取而代之的是,每个程序员都会为函数提出(通常不兼容并因此令人困惑)名称:“eval”、“evaluate”、“operation”、“doIt”等。因此清晰度降低了,因为我们被迫使用这些名称不要让他们的意思很明显。
转换为另一种类型的类:在 C++ 中,它是运算符 Type(),既可用于实际转换,也可用于产生所需类的内部成员。第二种情况在 Java 中经常出现,当一个类不必要地是 final 但你想向它添加操作时:
class DecoratedStringBuffer { //extends StringBuffer doesn't work, as String is final
private String byContainmentThen;
public decorate(final String prefix, final String suffix) { ... }
public append(final String s) { byContainmentThen.append(s);}
// other forwarding functions
}
由于 DecoratedStringBuffer 不是 is-a StringBuffer,在它离开你的代码并返回到客户端代码之前,它需要被转换回来,大概是通过一个最终应用后缀和前缀的函数。如果我们可以调用该运算符 StringBuffer() 那就太好了(如果 Java 像 C++ 一样可以应用一个用户提供的转换,那就更好了)。
相反,因为没有约定,我们必须给它一个必然更加模糊的名称。getStringBuffer() 是一个显而易见的名称,但对于许多 Java 用户来说,这意味着相应的 setStringBuffer,我们不想要它。即使它并不暗示,这个名字也是模棱两可的:你得到的 StringBuffer 是我们操作的那个,还是别的什么?
toStringBuffer() 是一个更好的名称,并且我倾向于应用这种模式,但是阅读代码的人想知道为什么看起来像 getter 的东西被称为“to”ClassName。
老实说,除了在设计数字类或“明显”可连接对象时,重载 op+ 几乎没有用处。由于 Java 不像 C++ 那样基于值,所以 op=; 没有太多用处。Java 并没有试图让一切都像一个原始的 int 值类。它是 op() 和我想念的转换运算符。
有功能很不错,但不是必不可少的。在许多情况下,我发现它们比它们的好处更令人困惑。就像在 JaredPars 示例中一样,.compare 函数对我来说比 '>' 更明显。可以直接看到 point1 是一个对象,而不是原始数据类型。我喜欢我使用的库中的重载运算符,但在我自己的代码中我很少使用它们。
编辑:我对函数名的选择不一致。将 .compare 替换为 .greaterThan 会更清楚。我的意思是,对我来说,绑定到对象的函数名称比乍一看与对象没有关联的运算符更明显。恕我直言,一个精心挑选的函数名称更容易阅读。