《C++覆辙录》——1.7:无视基础语言的精妙之处

    xiaoxiao2024-05-06  8

    本节书摘来自异步社区出版社《C++覆辙录》一书中的第1章,第1.7节,作者: 【美】Stephen C. Dewhurst(史蒂芬 C. 杜赫斯特),更多章节内容可以访问云栖社区“异步社区”公众号查看。

    1.7:无视基础语言的精妙之处

    大多数C++软件工程师都自信满满地认为自己对所谓C++的“基础语言”,也就是C++继承自C语言的那部分了如指掌。实际情况是,即使经验丰富的C++软件工程师,有时也会对最基础的C/C++语句和运算符的某些妙用一无所知。

    逻辑运算符不能算难懂,对吗?但刚入行的C++软件工程师却总是不能让它们物尽其用。你看到下面的代码时是不是会怒从胆边生?

    bool r = false; if( a < b )   r = true;``` 正解如下:

    bool r = a`

    int ctr = 0; for( int i = 0; i < 8; ++i )   if( options & 1<<(8+i) )     if( ctr++ ) {       cerr << "Too many options selected";       break;     }``` 何必这样如此费心地逐位比较?你忘记位屏蔽算法了吗?

    typedef unsigned short Bits;inline Bits repeated( Bits b, Bits m )   { return b & m & (b & m)-1; }// ...if( repeated( options, 0XFF00 ) )  cerr << "Too many options selected";`咳,现在的年轻人都怎么了,连这点布尔逻辑常识都没能好好掌握。

    还有,很多软件工程师都把“如果条件运算符表达式的两个选择结果都是左值,那么这个表达式本身就是个左值”这回事儿抛在脑后了(有关左值的讨论,参见常见错误6)。所以必然有些人就会写出如此代码:

    // 版本1 if( a < b )   a = val(); else if( b < c )   b = val(); else   c = val(); // 版本2 a``` 而对C++怀有正确观念的熟手稍加点化,上述代码马上变得短小精悍,简直酷毙了:

    // 版本3(a`如果你觉得这个貌似武林秘笈的小贴士似乎只是和布尔逻辑毫不相干的花拳绣腿,那么我得提醒你,在C++代码的很多上下文(比如构造函数的成员初始化列表,或抛出异常时throw表达式,等等)中,除了表达式别无选择。

    另有一点需要特别引起重视,就是在前两个版本中,val这个实体出现了不止一次,而在最后一个版本里,它只出现了一次。要是val是个函数的名字,那还好说。如果它是个函数宏,它的多次出现就极有可能带来非预期的副作用(常见错误26有更详细的讨论)。这种场合下,使用条件表达式而非if语句就并非可有可无的细节了。说实在的,我也不甚提倡这种结构被普遍使用,但我确实要大声呼吁这种结构要被普遍了解。对于想晋级专家级C++ 软件工程师的人们而言,这种用法必须在它能够大显身手时成为能够想到的工具之一。这也解释了它何以没有从C语言中被去掉而成为C++语言的一部分。

    让人惊讶的是,像内建的索引运算符居然也经常被误解。我们都知道数组的名字和指针都能够使用索引运算符。

    int ary[12]; int *p = &ary[5]; p[2] = 7;``` 此内建的索引运算符只是对于某些指针算术和提领运算符的一种简写法。像上面p[2]这个表达式和`*(p+2)`是完全等价的。从C的年代就一直在摸爬滚打的C++软件工程师都知道索引运算符的操作数可以是负数,所以p[-2]这样的表达式是有合式定义的,它不过就等价于`*(p-2)`,如果你愿意写成`*(p+-2)`也没问题。不过,似乎不是每个工程师都学好了加法交换律,否则为什么好多C++软件工程师看到下面这个表达式会吃惊不小? `(-2)[p] = 6;` 背后的变换极为平凡:p[-2]等价于`*(p+-2)`,后者等价于`*(-2+p)`,而`*(-2+p)`不就等价于`(-2)[p]`吗(上式中的圆括号不能省略,因为索引运算符的优先级比单目减法运算符要高)? 这究竟有何值得一提?的确值得一提!首先,此交换律仅适用于内建的索引运算符。所以,当我们看到形如6[p]的表达式时,我们就知道这里的索引运算符是内建的而不是用户自定义的(尽管p可能并不是指针或数组名)。还有,这样的表达式是你在鸡尾酒会上显摆的好谈资。当然了,在你一时冲动地把这种语法用到产品代码中之前,还是先静下心来看看常见错误11为妙。 大多数C++软件工程师都觉得`switch`语句是非常基础的。可惜他们不知道它能基础到何种地步。其实`switch`语句的形式语法就是如此平凡: ` switch( expression ) statement` 这平凡无奇的语法却能导出出人意料的推论。 典型情况是,`switch`表达式后面跟着一个语句区块。在这个语句区块里有一系列`case`标记的语句,然后根据计算决定跳转到这个语句区块的某个语句处执行。`C/C++`新手遇到的第一块绊脚石就是“直下式”(`fallthrough`)计算。也就是说和绝大多数语言不同的是,switch语句根据表达式的计算结果把执行点转到相应的`case`语句以后,它就甩手不管了。接下来执行什么,完全是软件工程师的事:

    switch( e ) { default: theDefault:  cout << "default" << endl;  // 直下式计算case 'a':case 0:  cout << "group 1" << endl;   break;case max-15:case Select<(MAX>12),A,B>::Result::value:   cout << "group 2" << endl;  goto theDefault;}`如果是有意去利用直下式计算的话——更多的人可能是由于疏忽才不小心让直下式计算引起了错误的执行流——我们习惯上要在适当的地方加上注释,提醒将来的维护工程师,我们这里使用直下式计算是有意为之的。不然,维护工程师就会像条件反射一样以为我们是漏掉了break语句,并给我们添上,这样就错了。

    记住,case语句的标签必须是整型常量性的表达式。换句话说,编译器必须能在编译期就算出这些表达式的值来。不过从上面这个例子你也应该能够看出,这些常量性的表达式能够用多么丰富多彩的写法来书写。而switch表达式本身一定是整型,或者有能够转换到整型的其他型别也可以。比如上面这个e就可以是个带有型别转换运算符的、能够转型到整型的class对象。

    同样要记住,switch语句的平凡语法暗示着我们能够把语句区块写成比上面的例子更非结构化的形式。在switch语句里的任何地方都能用case标记,而且不一定要在同一个嵌套层级里:

    switch( expr )   default:   if( cond1 ) {     case 1: stmt1;     case 2: stmt2;   }   else {     if( cond2 )       case 3:stmt2;     else       case 0: ;   }``` 这样的代码看起来有点傻(容我直言,确实很傻),但是这种对于基础语言边角部分的理解,有时候相当有用。比如利用上述的`switch`语句的性质,就曾在C++编译器中做出了一个复杂数据结构内部迭代的有效 实现:

    1.jpg gotcha07/iter.cpp

    bool Postorder::next() {  switch( pc ) case START:  while( true )   if( !lchild() ) {    pc = LEAF;   return true;    case LEAF:        while( true )         if( sibling() )           break;         else          if( parent() ) {             pc = INNER;            return true;    case INNER: ;         }         else {           pc = DONE;   case DONE:    return false;         }    }}`在上述代码中,我们使用了switch语句低级的、少见的语义来实现了树遍历操作。

    每当我使用上面这样的结构时,我总能收到强烈的、负面的甚至是骂骂咧咧的反应。而且我确实同意这种代码可不适合给维护工程师中的新手来打理,但是这样的结构——尤其是封装良好的、文档化了的版本——确实在对性能要求甚高或非常特殊的编码中有自己的一席之地。一句话,对基础语言难点的熟练掌握会对你大有裨益。

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)