我不明白反对运算符重载的论点


82

我刚刚读过Joel的其中一篇文章,他说:

通常,我必须承认我有点害怕隐藏事物的语言功能。当您看到代码时

i = j * 5;

…至少在C中,您知道j被乘以5,结果存储在i中。

但是,如果您在C ++中看到相同的代码片段,则您一无所知。没有。知道C ++中真正发生的事情的唯一方法是找出i和j是什么类型,这些类型可能在其他地方完全声明。这是因为j可能属于operator*重载类型,并且在您尝试将其乘以乘法时会表现出极大的机智。

(强调我。)害怕隐藏事物的语言功能吗?你怎么会害怕呢?隐藏事物(也称为抽象)不是面向对象编程的关键思想之一吗?每次调用方法时a.foo(b),您都不知道该怎么做。您必须找出什么类型ab是什么,然后才可能在其他地方声明它们。那么我们应该放弃面向对象的编程,因为它对程序员隐藏了太多的东西吗?

与可能有j * 5什么不同j.multiply(5),您可能必须使用不支持运算符重载的语言编写?再次,您将必须找出方法j内部的类型和peek multiply,因为lo和bekeeper j可能是具有某种multiply方法的类型,该方法执行某些非常机智的操作。

“ Muahaha,我是一个邪恶的程序员,只为一个方法命名multiply,但实际上它是完全晦涩且不直观的,并且与加乘运算绝对无关。” 设计编程语言时是否必须考虑这种情况?然后,我们不得不放弃编程语言中的标识符,理由是它们可能会误导人!

如果您想知道方法的作用,则可以浏览文档或浏览实现内部。运算符重载只是语法糖,我完全不知道它如何改变游戏。

请赐教。


20
+1:写得好,论据充分,有趣的话题且值得商de。p.se问题的一个生动示例。
Allon Guralnek 2010年

19
+1:人们听乔尔·斯波斯基(Joel Spolsky)是因为他写得好并且很出名。但这并不能使他100%地正确。我同意你的说法。如果我们所有人都遵循Joel的逻辑,我们将一事无成。
没人2010年

5
我认为i和j是在本地声明的,因此您可以快速查看它们的类型,或者它们是很烂的变量名,应适当地重命名。
卡梅隆·麦克法兰

5
+1,但不要忘了乔尔文章的最好的部分:在向正确答案跑了马拉松之后,他没有明显的理由停了50英尺。错误的代码不应该只是看起来错误而已。它不应该编译。
拉里·科尔曼

3
@Larry:您可以通过适当地定义类来使错误的代码无法编译,因此在他的示例中,您可以在C ++中使用SafeString和UnsafeString或RowIndex和ColumnIndex,但是您必须使用运算符重载才能使它们直观地表现。
David Thornley 2010年

Answers:


32

抽象的“隐藏”代码使您不必担心内部工作,而且经常也不必更改它们,但其目的并不是要阻止您查看它。我们只是对运营商做出假设,就像乔尔所说的那样,它可能在任何地方。具有要求在特定位置建立所有重载运算符的编程功能可能会有助于找到它,但是我不确定它会使使用它变得更容易。

我看不出*做比删除函数更类似于乘法的事情,而不是叫Get_Some_Data的函数。


13
+1对于“我看不到”位。语言功能可供使用,而不是滥用。
Michael K 2010年

5
但是,我们<<在流上定义了一个运算符,它与C ++标准库中的按位移位无关。
马尔科姆

仅出于历史原因才将其称为“按位移位”运算符。当应用于标准类型时,它会按位移位(与应用于数字类型时,+运算符将数字加在一起的方式相同),但是,当应用于复杂类型时,它可以做自己喜欢的事情那种类型的感觉。
gbjbaanb 2014年

1
*还用于取消引用(由智能指针和迭代器完成);目前尚不清楚好的过载与坏过载之间的界限在哪里
martinkunev 2015年

它不仅在任何地方,而且在j的类型定义中。
安迪

19

恕我直言,诸如运算符重载之类的语言功能赋予了程序员更大的权力。众所周知,强大的力量伴随着巨大的责任。具有更多功能的功能还为您提供了更多射击脚部的方法,显然,应谨慎使用。

例如,它可以完美的重载+*运算符class Matrixclass Complex。每个人都会立即知道这意味着什么。另一方面,对我来说+,即使是Java将其作为语言的一部分,而STL也是std::string使用运算符重载的,但这实际上并不意味着要串联字符串。

运算符重载使代码更清晰的另一个好例子是C ++中的智能指针。您希望智能指针的行为尽可能像常规指针一样,因此重载一元*->运算符是很有意义的。

本质上,运算符重载不过是命名函数的另一种方式。并且有一个函数命名规则:名称必须是描述性的,以使函数立即可见。相同的确切规则适用于运算符重载。


1
最后两个句子成为操作员重载反对的核心:对所有代码的渴望立即显而易见。
拉里·科尔曼

2
M * N的含义不是很明显,其中M和N是Matrix类型的吗?
迪马

2
@弗雷德:不。有一种矩阵乘法。您可以将一个mxn矩阵乘以一个nxk矩阵,以获得一个mxk矩阵。
迪马

1
@FredOverflow:有多种方法可以将三维向量相乘,一种给您标量,一种给您另一个三维向量,因此*,这些方法的重载会引起混淆。可以说,您可以operator*()将点乘积和operator%()叉乘积使用,但对于通用库,我不会这样做。
David Thornley 2010年

2
@马丁贝克特:号C ++是不允许重新排序A-BB-A任一,和所有运营商遵循图案。尽管总是有一个例外:当编译器可以证明这无关紧要时,就可以重新排列所有内容。
Sjoerd

9

在Haskell中,“ +”,“-”,“ *”,“ /”等仅仅是(中缀)函数。

您是否应该像“ 4加2”中那样将中缀函数命名为“加”?如果您的功能要做加法,那为什么不呢。您应该将“加号”功能命名为“ +”吗?为什么不。

我认为所谓的“运算符”的问题在于,它们大多类似于数学运算,并且解释它们的方式并不多,因此人们对这种方法/功能/运算符的功能寄予很高的期望。

编辑:使我的观点更加清楚


Erm除了从C,C ++继承的东西(这就是Fred所问的)之外,几乎都做同样的事情。现在您如何看待这是好是坏?
2010年

@sbi我喜欢操作符重载...其实即使C已超负荷运营商...您可以使用它们intfloatlong long和什么。那到底是什么呢?
2011年

@FUZxxl:这都是关于用户定义运算符重载内置运算符的。
2011年

1
@sbi Haskell在内置用户定义之间没有区别。所有运算符都是平等的。您甚至可以启用一些扩展功能,以删除所有预定义的内容,并让您从头开始编写任何内容,包括任何运算符。
2011年

@FUZxxl:可能是这样,但是那些相对的重载运算符通常不反对将内置+函数用于不同的内置数字类型,而是创建用户定义的重载。因此,我的评论。
2011年

7

根据我看到的其他答案,我只能得出结论,对运算符重载的真正反对是对立即显而易见的代码的渴望。

这很悲惨,原因有两个:

  1. 得出其逻辑结论后,代码应该立即显而易见的原则将使我们所有人仍然使用COBOL进行编码。
  2. 您不会从显而易见的代码中学习。您需要花一些时间来思考代码的工作原理,然后从有意义的代码中学习。

但是,从代码中学习并非始终是主要目标。在“功能X出现故障,编写它的人离开公司,而您需要尽快修复”这样的情况下,我宁愿拥有立即显而易见的代码。
Errorsatz

5

我有点同意。

如果编写multiply(j,5),则j可以是标量或矩阵类型multiply(),根据情况不同,其复杂程度或多或少j。但是,如果您完全放弃了重载的想法,则必须对函数进行命名,multiply_scalar()否则multiply_matrix()将使其变得显而易见。

在代码中,我们中的许多人会以一种方式来选择它,而在代码中我们大多数人会以另一种方式来使用它。但是,大多数代码都处于这两个极端之间的中间位置。在这里您更喜欢什么取决于您的背景和个人喜好。


好点子。但是,完全放弃重载在通用编程中效果
不佳

@FredO:当然不是。但是通用编程是针对完全不同的类型使用相同的算法,因此,那些偏爱multiply_matrix()通用编程的人也不会喜欢通用编程。
2010年

2
您对姓名颇为乐观,不是吗?根据我工作过的地方,我希望使用诸如“ multiply()`和'multiplym()` real_multiply()之类的名称。开发人员通常对名称不满意,operator*()至少会保持一致。
David Thornley 2010年

@David:是的,我跳过了名称可能不好的事实。但是,我们也可能会认为这operator*()可能会做一些愚蠢的事情,它j是对包含五个函数调用的表达式求值的宏,而并非如此。然后,您将无法再比较这两种方法。但是,是的,尽管要花点时间,但好好命名是很难的。
2010年

5
@David:由于命名很困难,应该从编程语言中删除名称,对吗?弄错它们太容易了!;-)
fredoverflow 2010年

4

我看到了运算符重载的两个问题。

  1. 重载会更改运算符的语义,即使程序员不打算这样做也是如此。例如,当你超载&&||或者,,你失去由这些运营商的内置变种暗示序列点(以及逻辑运算符的短路行为)。因此,即使语言允许,也最好不要重载这些运算符。
  2. 有些人认为操作员重载是一个很好的功能,即使不是合适的解决方案,他们也开始在各处使用它。这会导致其他人在另一个方向上反应过度,并警告不要使用操作员重载。我不同意这两个小组,但要采取中间立场:运算符重载应谨慎使用,并且仅当
    • 对于域专家和软件专家而言,重载的运算符具有自然的含义。如果这两个组对操作员的自然含义不一致,请不要重载。
    • 对于所涉及的类型,运算符没有自然的含义,并且直接上下文(最好是相同的表达式,但不超过几行)始终使运算符的含义清楚。此类别的一个示例是operator<<流。

1
向我+1,但第二个参数同样可以很好地应用于继承。许多人对继承一无所知,并试图将其应用于所有事物。我认为大多数程序员都会同意可能滥用继承。这是否意味着继承是“邪恶的”,应该从编程语言中放弃?还是应该保留它,因为它也可能有用?
fredoverflow

@FredOverflow第二个参数可以应用于任何“新的和热门的”。我并不是将其作为消除语言中的运算符重载的参数,而是出于人们反对它的原因。就我而言,运算符重载是有用的,但应谨慎使用。
Bart van Ingen Schenau 2010年

恕我直言,允许重载&&并且||以不暗示排序的方式是一个大错误(恕我直言,如果C ++要允许这些重载,它应该使用特殊的“双功能”格式,并且需要第一个功能返回隐式可转换为整数的类型;第二个函数可以使用两个或三个参数,第二个函数的“ extra”参数是第一个函数的返回类型。编译器将调用第一个函数,然后,如果它返回非零值,则求值第二个操作数,并对其调用第二个函数。)
supercat

当然,这不像允许逗号运算符重载那样奇怪。顺便说一句,我实际上并没有看到过但想看到的一种重载的东西,它是一种紧密绑定成员访问的方法,它允许像的类那样foo.bar[3].X处理表达式foo,而不是要求foo公开一个可以支持下标,然后公开一个成员X。如果要通过实际成员访问来强制进行评估,则可以写((foo.bar)[3]).X
超级猫

3

根据我的个人经验,Java允许多种方法但不允许重载运算符的方式意味着您每次看到一个运算符时都会确切知道它的作用。

您不必查看是否*调用奇怪的代码,而是知道它是一个乘法,并且其行为与Java语言规范所定义的方式完全相同。这意味着您可以专注于实际行为,而不是找出程序员定义的所有检票口东西。

换句话说,禁止操作员重载对读者(而不是写作者)有益,因此使程序更易于维护!


请注意,+ 1:C ++为您提供了足够的绳索来吊住自己。但是,如果我想在C ++中实现链接列表,我希望能够使用[]访问第n个元素。将运算符用于对其有效的数据(从数学上来说)是有意义的。
Michael K 2010年

@Michael,您不能接受list.get(n)语法吗?

@Thorbjørn:其实也很好,也许是一个不好的例子。时间可能更好-重载+,-会比time.add(anotherTime)有意义。
Michael K 2010年

4
@Michael:关于链接列表,std::list不会重载operator[](或提供任何其他索引到列表的方法),因为这样的操作将是O(n),并且如果您关心效率,则列表接口不应公开此类函数。客户端可能会尝试遍历带有索引的链表,从而使O(n)算法不必要地成为O(n ^ 2)。您经常在Java代码中看到这种情况,尤其是当人们使用List旨在完全消除复杂性的界面时。
fredoverflow

5
@Thor:“但是为了确定您必须检查:)” ...同样,这与操作符重载无关。如果看到time.add(anotherTime),则还必须检查库程序员是否“正确”实现了添加操作(无论如何)。
fredoverflow

3

重载a * b和调用之间的区别multiply(a,b)是后者很容易被抢占。如果该multiply函数未针对不同类型进行重载,则可以准确地找到该函数将要执行的操作,而不必跟踪a和的类型b

Linus Torvalds 关于运算符重载有一个有趣的论点。在诸如Linux内核开发之类的系统中,大多数更改是通过电子邮件通过补丁发送的,维护人员必须了解补丁在每个更改周围只有几行上下文的作用,这一点很重要。如果没有重载函数和运算符,则可以更轻松地以上下文无关的方式读取补丁,因为您不必遍历更改的文件来找出所有类型是什么,并检查重载的运算符。


linux内核不是用纯C语言开发的吗?为什么要在这种情况下讨论(运算符)重载?
fredoverflow

对于具有相似开发过程的任何项目,无论其语言如何,关注点都是相同的。如果您只需要打补丁文件中的几行,那么过多的过载将使您难以理解更改的影响。
斯科特·威尔士

@FredOverflow:Linux内核确实在GCC C中。它使用各种扩展,有时会给C带来几乎C ++的感觉。我在想一些奇特的类型操作。
Zan Lynx

2
@斯科特:关于用C编程的项目讨论重载的“弊端”是没有意义的,因为C没有重载函数的能力。
fredoverflow

3
在我看来,莱纳斯·托瓦尔兹(Linus Torvalds)的观点狭窄。他有时会批评那些对Linux内核编程实际上没有用的东西,好像使它们不适合一般使用。颠覆就是一个例子。这是一个不错的VCS,但是Linux内核开发确实需要分布式VCS,因此Linus普遍批评SVN。
David Thornley 2010年

2

我怀疑这与超出预期有关。我已经习惯了C ++,并且习惯了操作员的行为并不完全受语言的支配,并且当操作员做一些奇怪的事情时,您不会感到惊讶。如果您习惯于不具备该功能的语言,然后再看C ++代码,那么您会带来其他语言的期望,当发现重载的运算符做一些时髦的事情时,可能会感到惊讶。

我个人认为有区别。当您可以更改语言的内置语法的行为时,它的推理就变得更加不透明。不允许元编程的语言在语法上不那么强大,但在概念上更易于理解。


重载的运算符永远不要做“奇怪的事情”。当然,如果它做复杂的事情也很好。但是只有当它具有一个明显的含义时才重载。
Sjoerd

2

我认为重载数学运算符并不是C ++中运算符重载的真正问题。我认为不应该依赖表达式(即类型)上下文的重载运算符是“邪恶的”。例如超载, [ ] ( ) -> ->* new delete,甚至一元*。这些运营商对您有一定的期望,这些期望永远都不会改变。


+1不要使[]等同于++。
Michael K 2010年

3
你是说我们不应该能够超载你提到的运营商在所有?还是您只是在说我们应该仅出于理智的目的而使它们过载?因为我不希望看到不带容器,不带operator[]函子,不带operator()智能指针operator->等。
fredoverflow

我的意思是,与那些运算符相比,数学运算符对运算符重载的潜在问题很小。这样做的很漂亮或疯狂的数学运算可能会麻烦,但我列出的运营商,这人平时不觉得,作为运营商,而是基本的语言元素,必须始终符合语言定义的期望。[]应始终是类似数组的->访问器,并且应始终意味着访问成员。它实际上是数组还是其他容器,或者是否是智能指针,都没有关系。
Allon Guralnek 2010年

2

我完全理解您不喜欢乔尔关于藏身的说法。我也不。对于内置数字类型之类的东西或您自己的矩阵之类的东西,最好使用“ +”号。我承认,将两个矩阵与'*'而不是'.multiply()'相乘是一种简洁而优雅的做法。毕竟,在两种情况下我们都具有相同的抽象。

这里的问题是代码的可读性。在现实生活中,不是在矩阵乘法的学术示例中。例如,尤其是如果您的语言允许定义语言核心中最初不存在的运算符=:=。此时,还会出现很多其他问题。那该死的操作员是做什么的?我的意思是那件事的优先顺序是什么?什么是关联性?a =:= b =:= c真正执行的顺序是?

这已经是反对运算符重载的论点。还是不服气?检查优先级规则只花了您10秒钟?好吧,让我们走的更远。

如果您开始使用允许运算符重载的语言,例如流行的名称以“ S”开头的语言,您将很快了解到库设计人员喜欢重写运算符。当然,他们受过良好的教育,他们遵循最佳实践(这里没有玩世不恭),当我们分别看待它们时,所有它们的API都很有意义。

现在想象一下,您必须使用一些API,这些API大量使用了在单个代码中一起重载运算符的方式。甚至更好-您必须阅读一些类似的旧代码。这是操作员超负荷确实很糟糕的时候。基本上,如果一个地方有很多重载运算符,它们很快就会开始与程序代码中的其他非字母数字字符混合。它们将与非字母数字字符混合在一起,这些字符不是真正的运算符,而是一些更基本的语言语法元素,这些元素定义了诸如块和作用域,形状流控制语句之类的东西或表示一些元事物。您需要戴上眼镜并将眼睛移近LCD显示屏10厘米,以了解视觉混乱。


1
重载现有的运算符和发明新的运算符不是一回事,而是我的+1。
fredoverflow

1

通常,我避免以非直观的方式使用运算符重载。也就是说,如果我有一个数值类,重载*是可以接受的(并鼓励)。但是,如果我有一个Employee类,重载*会做什么?换句话说,以直观的方式重载运算符,使其易于阅读和理解。

可接受/鼓励:

class Complex
{
public:
    double r;
    double i;

    Complex operator*(const Compex& rhs)
    {
        Complex result;
        result.r = (r * rhs.r) - (i * rhs.i);
        result.i = (r * rhs.i) + (i * rhs.r);
        return result;
    }
};

不能接受的:

class Employee
{
public:
    std::string name;
    std::string address;
    std::string phone_number;

    Employee operator* (const Employee& e)
    {
        // what the hell do I do here??
    }
};

1
乘员人数?当然,如果他们在会议室的桌子上这样做,那就是可以解雇的罪行。
gbjbaanb

1

除了在这里已经说过的话,还有一个反对运算符重载的论点。确实,如果您编写+,这很明显意味着您要在某些内容上添加一些内容。但这并非总是如此。

C ++本身就是一个很好的例子。stream << 1应该如何阅读?流向左移1?除非您明确知道C ++中的<<也会写入流,否则这根本不是显而易见的。但是,如果将此操作实现为方法,那么任何明智的开发人员都不会编写o.leftShift(1),就像o.write(1)

最重要的是,通过使运算符重载不可用,该语言使程序员可以考虑操作的名称。即使选择的名称不是完美的名称,误解名称也仍然比符号更难。


1

与详细说明的方法相比,运算符要短一些,但也不需要括号。括号相对不方便键入。而且您必须平衡它们。总的来说,与操作员相比,任何方法调用都需要三个字符的普通噪声。这使得使用运算符非常诱人。
为什么还要有人这样做cout << "Hello world"呢?

重载的问题在于,大多数程序员都非常懒惰,而大多数程序员负担不起。

使C ++程序员滥用运算符重载的原因不是它的存在,而是缺少执行方法调用的更整洁的方法。人们不仅担心操作员超载是可能的,而且还因为这样做已经完成。
请注意,例如在Ruby和Scala中,没有人担心操作符重载。除了事实之外,运算符的使用并不比方法更短,另一个原因是,Ruby 将运算符的重载限制在一个合理的最小值,而Scala 允许您声明自己的运算符,从而避免了琐碎的冲突。


或者,在C#中,使用+ =将事件绑定到委托。我不认为将语言功能归咎于程序员的愚蠢是一种建设性的前进方式。
gbjbaanb

0

运算符重载之所以令人恐惧,是因为有许多程序员甚至从未想过这*并不意味着简单地“乘法”,而诸如此类的方法foo.multiply(bar)至少立即指出该程序员有人编写了自定义乘法方法。在这一点上,他们想知道为什么要去调查。

我曾与处于“高级”职位的“高级程序员”一起工作,他们将创建称为“ CompareValues”的方法,该方法将接受2个参数,并将值从一个应用于另一个,然后返回布尔值。或称为“ LoadTheValues”的方法将进入数据库以获取其他3个对象,获取值,进行计算,修改this并将其保存到数据库。

如果我正在与这些类​​型的程序员一起工作,则我立即知道要调查他们所从事的工作。如果他们让操作员超负荷工作,除了假定他们照做并继续寻找外,我根本不知道他们做了。

在一个完美的世界中,或者在一个拥有完美程序员的团队中,操作员重载可能是一个很棒的工具。不过,我还没有在一个由完美的程序员组成的团队中工作,所以这很令人恐惧。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.