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

一般来说,我不得不承认我有点害怕隐藏事物的语言功能。当您在C语言中看到代码
i = j * 5;

...时,至少知道j被乘以5并将结果存储在i中。
但是如果您看到相同的代码段在C ++中,您一无所知。没有。知道C ++中真正发生的事情的唯一方法是找出i和j是什么类型,这些类型可能会在其他地方声明。这是因为j可能是operator*重载的类型,并且在尝试将其乘以乘积时会做一些非常机灵的事情。

(强调我。)害怕隐藏事物的语言功能吗?你怎么会害怕呢?隐藏事物(也称为抽象)不是面向对象编程的关键思想之一吗?每次调用方法a.foo(b)时,您都不知道该怎么做。您必须找出ab是什么类型,哪些可能在其他地方一起声明。那么我们应该放弃面向对象的编程,因为它对程序员隐藏了太多的东西吗?
j * 5j.multiply(5)有什么不同,您可能必须使用不支持运算符重载的语言来编写它?再次,您将不得不找出j的类型并在multiply方法中进行窥视,因为请注意,j可能是具有multiply方法的类型,可以执行某些非常机智的操作。
“ Muhahaha,我是一个邪恶的程序员,将方法命名为multiply,但是它的实际作用是完全晦涩且不直观的,并且与乘法运算完全无关。”设计编程语言时是否必须考虑这种情况?然后,我们必须以可能会误导用户的身份放弃编程语言中的标识符!
如果您想知道方法的作用,则可以浏览文档或浏览实现内部。运算符重载只是语法糖,我完全不知道它如何改变游戏。
请赐教。

评论

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

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

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

+1,但不要忘了乔尔文章的最好的部分:在对正确答案进行马拉松比赛后,他没有明显的理由停下50英尺。错误的代码不应该看起来只是错误的。它不应该编译。

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

#1 楼

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

我看不到*做比起删除数据的Get_Some_Data函数,它与乘法没有什么相似之处。

评论


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

– Michael K
2010-12-10 13:37

然而,我们在流上定义了<<操作符,它与C ++标准库中的按位移位无关。

–马尔科姆
2013年7月25日在16:32



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

– gbjbaanb
2014年4月7日在9:33

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

–martinkunev
2015年2月9日,12:57

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

–安迪
2015年10月29日在1:07

#2 楼

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

例如,将+*class Matrixclass Complex运算符重载是非常合理的。每个人都会立即知道这意味着什么。另一方面,对我而言,即使Java将+作为语言的一部分,而std::string意味着字符串的连接也并不明显,而STL使用运算符重载对*进行了编码。

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

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

评论


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

–拉里·科尔曼(Larry Coleman)
2010-12-10 16:00

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

–迪马
2010-12-10 16:16

@弗雷德:不。有一种矩阵乘法。您可以将一个m x n矩阵乘以一个n x k矩阵,以获得一个m x k矩阵。

–迪马
2010-12-10 18:35

@FredOverflow:有多种方法可以将三维向量相乘,一种给您标量,另一种给您另一个三维向量,因此对于那些重载*可能会造成混乱。可以说,您可以对点积使用operator *(),对叉积使用operator%(),但是对于通用库,我不会这样做。

– David Thornley
2010-12-10 18:48

@马丁·贝克特(Martin Beckett):不。C++也不允许将A-B重新排序为B-A,并且所有运算符都遵循该模式。尽管总是有一个例外:当编译器可以证明无关紧要时,便可以重新排列所有内容。

– Sjoerd
2011年4月10日在7:13

#3 楼

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

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

我认为所谓的“运算符”的问题在于,它们主要类似于数学运算,并且解释它们的方式并不多,因此人们对这种方法/函数/运算符可以。

编辑:我的观点更加清楚

评论


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

–sbi
2010-12-10 12:39

@sbi我喜欢运算符重载...实际上,甚至C也有运算符重载...您可以将它们用于int,float,long long和任何类型。那到底是什么呢?

– FUZxxl
2011年10月8日14:04

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

–sbi
2011年10月8日14:12

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

– FUZxxl
2011年10月8日14:18

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

–sbi
2011-10-8 14:23

#4 楼

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

这是可悲的,有两个原因:


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


评论


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

–Errorsatz
19年5月24日在19:01

#5 楼

我有点同意。

如果编写multiply(j,5),则j可以是标量或矩阵类型,这取决于multiply()是什么,使j或多或少复杂。但是,如果您完全放弃重载的想法,那么该函数将必须命名为multiply_scalar()multiply_matrix(),这将使显而易见的情况发生在下面。

有些代码使我们许多人偏爱它,有些代码使我们大多数人偏爱它。但是,大多数代码都处于这两个极端之间的中间位置。您在那里喜欢什么取决于您的背景和个人喜好。

评论


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

– fredoverflow
2010-12-10 12:20

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

–sbi
2010-12-10 13:04

您对姓名颇为乐观,不是吗?根据我工作过的地方,我希望使用“ multiply()”和“ multiplym()”或“ real_multiply()”之类的名称。开发人员通常对名称不满意,并且operator *()至少要保持一致。

– David Thornley
2010-12-10 18:56

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

–sbi
2010-12-10 19:21

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

– fredoverflow
2010-12-11在8:04



#6 楼

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


重载改变了运算符的语义,即使程序员不打算这样做也是如此。例如,当重载&&||,时,您将丢失这些运算符的内置变量所隐含的序列点(以及逻辑运算符的短路行为)。因此,即使语言允许,也最好不要重载这些运算符。
有人认为运算符重载是一个很好的功能,即使不是合适的解决方案,他们也开始在各处使用它。 。这会导致其他人朝另一个方向过度反应,并警告不要使用运算符重载。
我不同意这两个团体,但要采取中间立场:运算符重载应谨慎使用,并且仅当

对于域专家和软件专家来说,重载运算符具有自然的含义。如果这两个组对运算符的自然含义不一致,请不要重载。
对于所涉及的类型,对于运算符和直接上下文(最好是相同的表达式, (但不超过几行)总是可以清楚地说明运算符的含义。此类别的示例是流的operator<<




评论


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

– fredoverflow
2010-12-10 17:42



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

–巴特·范·恩根·舍瑙(Bart van Ingen Schenau)
2010-12-14 10:50

恕我直言,允许&&和||的重载在某种程度上,这并不意味着排序是个大错误(恕我直言,如果C ++要允许这些代码重载,它应该使用特殊的“双功能”格式,要求第一个函数返回一个类型为隐式地可转换为整数;第二个函数可以使用两个或三个参数,第二个函数的“ extra”参数是第一个函数的返回类型,编译器将调用第一个函数,如果返回非零,则调用该函数,求出第二个操作数,并在其上调用第二个函数。)

–超级猫
2012年7月19日在15:54



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

–超级猫
2012年7月19日在16:02

#7 楼

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

您不必查看*是否调用奇怪的代码,但知道它是一个乘法,并且其行为与Java语言规范中定义的方式完全相同。这意味着您可以专注于实际行为,而不是找出程序员定义的所有错误信息。换句话说,禁止操作员重载对读者(而不是作者)有好处,因此使程序更易于维护!

评论


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

– Michael K
2010-12-10 13:41

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

–user1249
2010-12-10 13:44

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

– Michael K
2010-12-10 13:50

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

– fredoverflow
2010-12-10 17:23



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

– fredoverflow
2010-12-10 17:35

#8 楼

重载a * b和调用multiply(a,b)之间的区别是,可以很容易地重载后者。如果multiply函数没有针对不同类型进行重载,则您可以准确地找到该函数的功能,而不必跟踪ab的类型。

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

评论


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

– fredoverflow
2010-12-10 13:26

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

–斯科特威尔士
2010-12-10 13:34

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

–赞·山猫
2010-12-10 17:39

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

– fredoverflow
2010-12-10 17:47

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

– David Thornley
2010-12-10 18:42

#9 楼

我怀疑这与超出预期有关。我已经习惯了C ++,并且习惯了操作员的行为并不完全由语言决定,并且当操作员做一些奇怪的事情时,您不会感到惊讶。如果您习惯于不具备该功能的语言,然后再看C ++代码,那么您会带来其他语言的期望,当发现重载的运算符做一些时髦的事情时,可能会感到惊讶。 >
我个人认为有区别。当您可以更改语言的内置语法的行为时,它的推理就变得更加不透明。不允许元编程的语言在语法上不那么强大,但在概念上更易于理解。

评论


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

– Sjoerd
2011年4月10日在7:18

#10 楼

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

评论


+1不要使[]等同于++。

– Michael K
2010-12-10 13:38

您是在说我们根本不应该让您提到的操作员超负荷吗?还是您只是在说我们应该仅出于理智的目的而使它们过载?因为我不希望看到没有operator []的容器,没有operator()的函子,没有operator->的智能指针等。

– fredoverflow
2010-12-10 17:39



我的意思是,与那些运算符相比,数学运算符对运算符重载的潜在问题很小。用数学运算符执行聪明或疯狂的操作可能会很麻烦,但是我列出的运算符通常应该符合语言定义的期望,而人们通常不将其视为运算符,而是基本的语言元素。 []应该始终是类似数组的访问器,而->应该始终意味着要访问成员。它实际上是数组还是不同的容器,或者它是否是智能指针都没有关系。

– Allon Guralnek
2010-12-10 21:44



#11 楼

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

C ++本身就是一个很好的例子。 stream << 1应该如何读取?流向左移1?除非您明确知道C ++中的<<也会写入流,否则这根本不是显而易见的。但是,如果将此操作实现为方法,则没有任何精明的开发人员会编写o.leftShift(1),就像o.write(1)一样。操作名称。即使所选择的名称不是完美的名称,误解名称也仍然比符号更难。

#12 楼

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

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

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

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

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

评论


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

– fredoverflow
15年4月16日在17:32

#13 楼

通常,我避免以非直观的方式使用运算符重载。就是说,如果我有一个数值类,重载*是可以接受的(并鼓励)。但是,如果我有一个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??
    }
};


评论


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

– gbjbaanb
2011年10月8日23:48

#14 楼

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

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

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

评论


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

– gbjbaanb
2011年10月8日23:50

#15 楼

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

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

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

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