翻译《有关编程、重构及其他的终极问题?》——11.不要试图把尽量多的操作符放到一行代码里

本文讨论了将过多操作符置于同一行代码中所带来的问题,特别是undefined behavior,并提供了改善代码清晰度和可维护性的建议。

翻译《有关编程、重构及其他的终极问题?》——11.不要试图把尽量多的操作符放到一行代码里

标签(空格分隔): 翻译 技术 C/C++
作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
最后更新:2016年12月19日


本书背景说明、总目录等介绍,可以跳转到以下链接进行查看:
http://blog.youkuaiyun.com/headman/article/details/53045891

欢迎大家转载,但请附上原作者以及翻译者的名字、原文出处,以尊重光荣的劳动者。


11.不要试图把尽量多的操作符放到一行代码里

下面这段代码来自Godot Engine项目。PVS-Studio诊断显示的错误为:V567 Undefined behavior. The ‘t’ variable is modified while being used twice between sequence points(译者注:大意是说发现未定义的行为。当在序列点之间被使用了两次时,变量t被修改了)。

static real_t out(real_t t, real_t b, real_t c, real_t d)
{
    return c * ((t = t / d - 1) * t * t + 1) + b;
}

解释
有时候,你在一些代码中可以看到作者尽量用复杂的结构把更多的逻辑操作塞到很小的代码空间中。这种行为很难说是否对编译器有帮助,但却让代码变得对其他程序员(甚至是作者自己)而言难以阅读和理解。而且,在这种代码中,发生错误的几率也更高。

在这种程序员想把更多的逻辑操作用尽量少的代码来表述时,就会经常产生名为undefined behavior的错误。这种错误一般来说,是因为在一个sequence point中必须要同时对一个变量进行读、写操作造成。为了更好的理解这个问题,我们需要讨论一下更多关于“undefined behavior”和“sequence point”的细节。

Undefined behavior一般是指某些编程语言的一个属性,即表明需要依赖于具体编译器的执行或者优化情况才能确定结果。相当多的undefined behavior例子(包括我们正在这里讨论的)都和“sequence point”紧密相关。

一个sequence point定义了一个计算机程序的执行点,在这个点上,可以保证之前的所有表达式都已经执行完成,后续的所有表达式没有任何地方被执行。在C/C++中有如下的sequence point:

  • 针对操作符”&&”,“||”,“,”的sequence point。只要没有被重载,这些操作符就能保证从左到右的执行顺序;
  • 针对三元操作符“?:”的sequence point;
  • 在每个完整表达式结尾的sequence point(通常用‘;’表示);
  • 在函数调用处的sequence point,一般在完成参数计算后;
  • 当函数返回时的sequence point。

注意:在新的C++规范中(译者注:应该是在C++11以及之后的规范中)已经取消了“sequence point”的概念,当我们依然会采用上面给出的解释让那些不熟悉这个知识的程序员们可以容易和快速的抓住重点。这个解释可比新规范的解释更简单,并且也足够我们去理解为什么不要把很多操作放到一个“管子”中。

在一开始的例子中,没有上面所提到的任何sequence point,不管是’=’操作符还是括号,都不能被认为是sequence point。因此,当计算返回值时,我们不知道到底哪一个t变量被使用了。

换句话说,这整个表达式其实是一个sequence point,因此我们不知道t变量到底是按照什么顺序被访问的。例如,我们无法确认“t * t”子运算式到底在“t = t / d - 1”之前还是之后计算。

正确的代码

static real_t out(real_t t, real_t b, real_t c, real_t d)
{
    t = t / d - 1;
    return c * (t * t * t + 1) + b;
}

建议
这很显然,试图把整个复杂的表达式现在一行里不是个好主意。除此之外,还会造成阅读困难,而且让错误有机可乘。

我通过把表达式拆成两部分就一次解决了这个问题——让代码变得可读性更强,而且通过增加了一个sequence point而去除了undefined behavior错误。

上面的代码不是唯一的例子,这里还有一个:

*(mem + addr++) = 
   (opcode >= BENCHOPCODES) ? 0x00 : ((addr >> 4) + 1) << 4;

也类似前面的例子,这里的错误是由不合理的复杂代码引起的。当这个程序员试图在这个表达式内增加addr变量的值时,就导致了undefine behavior错误,这是因为编译器不知道表达式左边addr的值到底是原来的值还是自增后的值。

和之前一样,解决这个问题的最好方式是不要莫名其妙的使事情复杂化,把不同的操作简化到多条表达式中,而不是都放在一个表达式中:

*(mem + addr) = (opcode >= BENCHOPCODES) ? 0x00 : ((addr >> 4) + 1) << 4; 
addr++;

我们可以从所有这些问题中抽象出一个更简单的结论来:只要有可能,就不要试图把一组复杂的操作符放到很少的代码中。最好的实践就是把代码分成多个片段,这样不仅可以提高可理解性,而且可以减少犯错的机会。

下次,如果你想写复杂结构的代码,请暂停一会儿去思考这会给你带来多少代价,而你又准备复出多少代价。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值