逗号运算符(C++)的微妙危险

2020-05-30 15:03:51

乔纳森·博卡拉(Jonathan Boccara)写的,但就像一位同时也是超级英雄叔叔的哲学家曾经说过的那样,权力越大,责任就越大。

翻译成C++,这意味着如果您不小心,一些让您编写富于表现力的代码的C++功能可能会掉头并产生错误代码,而这些代码并没有做它应该做的事情。

一个很好的例子(美丽的一些定义)是逗号操作符的重载。正如我们将要看到的,工作代码中一个非常细微的变化就会使它变得非常错误。

首先,逗号操作符是一个东西。它对所有类型的默认实现都是这样做的:a,b计算a,然后计算b,然后返回b。例如,1,2返回2。

通常不推荐这样做,但是C++允许重载逗号操作符。下面是一篇关于重载逗号操作符的详细文章,您可以阅读该文章以更好地了解该主题。

重载逗号操作符可以让我们做一些好事。例如,这是Boost Assign用来允许我们将数据追加到现有向量中的内容:

如果不重载逗号运算符,我们就不能用标准C++编写此表达式,即使在C++20中也是如此。一旦构造了向量,我们只能使用PUSH_BACK成员函数逐个添加元素。

前面的代码允许我们用非常有表现力的代码向现有向量添加元素。

下面是Boost Assign的一个非常简化的实现,它允许我们编写前一行代码(感谢此实现的Nope):

Template<;TypeName Vector>;struct{Vector&;vec;Template<;TypeName T>;Appder<;Vector>;Operator,(const T&;e){vec.Push_back(E);return*this;}};Template<;TypeName T>;Appder<;std::Vector<;T>;>;Operator+=(STT。e){v.push_back(E);return{v};}int(){auto data=std::Vector<;int>;{};data+=1,2,3,4,5;对于(auto&;&;e:data)std::cout<;<;';<;<;e;std::cout<;<;';\n。

附加器重载逗号运算符。当此附加器与2关联时,它会将其添加到向量中并返回自身。

然后,附加器与3相关联并将其添加,然后是4,然后是5。

现在,让我们对我们的代码做一点小小的修改。让我们将逗号操作符定义为自由函数,而不是将其定义为成员函数。例如,这可能是可取的,因为它允许隐式转换,如Efficient C++的第24项中所述。

Template<;TypeName Vector>;struct{Vector&;vec;};Template<;TypeName Vector,TypeName T>;Appder<;Vector>;&;Operator,(Appder<;Vector>;&;v,Const T&;e){v.vec.Push_back(E);return v;}Template<;TypeName T>;Appder<;ST.。v,const T&;e){v.ush_back(E);return{v};}int(){auto data=std::Vector<;int>;{};data+=1,2,3,4,5;对于(auto&;&;e:data)std::cout<;<;';&39;<;<;e;std::cout<;&ld。

如果你和我一样,你会难以置信地盯着屏幕。如果你想亲眼看看,就运行能运行的程序和不能运行的程序。

也许最糟糕的是它会编译,而不是它没有我们自然期望的行为。你能明白为什么会发生这种事吗?

说真的,试着自己找出哪里出了问题。我稍后会告诉你,但你自己去找会更有趣、更有价值。

你在用手机,不方便吗?请不要担心,请将此页面加入书签或通过电子邮件发送给您自己,这样您可以稍后在计算机上返回该页面。

这个问题与左值和右值有关。如果我们再看一下自由函数运算符,它会将左值引用作为输入:

数据+=1是右值。左值引用不能绑定到它。因此,不会调用逗号运算符的此重载。

如果它是任何其他运算符,代码就不会编译。但是,正如我们在本文开头看到的,逗号操作符对所有类型都有一个默认实现。因此,执行默认实现-返回第二个元素,这里是2。然后返回3,然后是4,然后是5。而且它实际上没有做任何事情。

不正确地重载逗号运算符会导致静默失败。代码可以编译、运行,但不会执行您想要的操作。

要使此实现正常工作,我们需要提供可以接受r值的逗号操作符的重载:

Template<;TypeName Vector,TypeName T>;Appder<;Vector>;&;Operator,(Appder<;Vector>;&;v,const T&;e){v.Vector_back(E);return v;}Template<;TypeName Vector,TypeName T>;Appder<;Vector>;&;Operator,(Appder<;Vector>(E);return v;}Template<;TypeName Vector,TypeName T>;Appder<;Vector>;&;Operator,(Appder<;Vector>。返回v;}

要在我们的示例中使用它,我们需要返回附加器的副本,以便常量引用可以绑定到它。在我们的示例中,这仍然会附加到向量中,因为附加器的各种副本将包含对同一向量的引用(感谢Patrice Dalesme向我展示了这个解决方案):

第一个是,如果我们重载逗号操作符,我们需要格外小心地涵盖所有情况,并考虑左值和右值。否则,我们最终会得到错误代码。

第二个独立于逗号运算符。我们看到,在C++中,成员函数比自由函数更容易定义。默认情况下,成员函数是为类型的左值和右值引用定义的,而接受引用的自由函数可能只适用于一种情况。

也存在相反的情况(为左值或右值显式定义的成员函数,以及通过复制或常量引用获取的自由函数),但最常见的原型具有我们前面讨论过的属性。

Jonathan Boccara是C++软件工程负责人、博客作者和作家,专注于如何使代码具有表现力。他的博客是流畅的C++。

布尔玛是一位多才多艺的自由插画家,风格古怪,五颜六色。布尔玛热爱动画,主要为运动图形做插图,利用她的创造力帮助品牌讲述故事。布尔马来自立陶宛,曾在英国学习和工作,现在马耳他享受着岛屿生活。在那里,她学会了游泳,并更深入地从事插图工作。布尔玛一直在寻找新的挑战和机遇。