C++ Gotchas 条款1及条款17

    技术2022-05-11  181

    C++ Gotchas 条款1及条款17

    Stephen C. Dewhurst

     

    Gotcha条款1:过渡注释

     

    有许多注释其实是不必要的。其一般都会使得源代码难于阅读和维护,并常常将维护人员引入歧途。考查下面这个简单的语句:

     

    a = b;  // b 赋值给 a

     

    比起代码本身,这个注释并不能传达更清晰的语句含义,因此是没有用的。实际上,它比没有用还糟。它是致命的。其一,该注释转移了读者投向代码的注意力,增加了读者为获知代码含义所要费心过目的文字量;其二,导致有更多的文本需要维护,因为注释必须随着其对应代码的改变而改变以得到维护;其三,这种对注释的必要维护经常没有被实施。

     

    c = b; // b 赋值给 a

     

    一位细心的维护人员不会简单的认为该注释存在谬误,因而不得不通过追踪程序来确认注释是否是错误的、非常规的(c是指向areference)或微妙的(对c赋值会导致后来将相同的赋值传播给a)。该行代码本来应该不需要注释:

     

    a = b;

     

    该代码就其本身而言是足够清晰的,没有会被不当维护的注释。这在道义上近似于“最高效的代码就是那些不存在的代码”这句陈年老话。同样的道理适用于注释:最好的注释就是那些不必被撰写的注释,因为代码本身是自文档化的(self-documenting)(若非如此,则早就用注释来描述它了)。

     

    另一些常见的有关不必要注释的示例频繁出现在类别定义里,其要么是糟糕的编码规范造成的恶果,要么就是C++菜鸟的杰作。

     

    class C {

     // Public Interface

     public:

       C(); // default constructor

       ~C(); // destructor

       // . . .

    };

     

    你感觉你好像在阅读某人的幼儿手稿。如果一个维护人员需要被时刻提示public:的含义,你一定不希望由这个人来维护你的代码。上述注释没有哪一条能为有经验的C++程序员提供任何帮助,只会给代码添乱并提供更多会被不当维护的文本。

     

    class C {

     // Public Interface

     protected:

       C( int ); // default constructor

     public:

       virtual ~C(); // destructor

       // . . .

    };

     

    程序员总有很强烈的动机,不希望“浪费”源代码文本的行数。据称,如果一个建构单位(函数、类别的公共接口,等等)能够以惯用且合理的格式在一个约3040行的“页面”上面显示出来,那么其代码就很容易理解;如果该代码延长至第二页,理解它的难度就是原来的两倍;而如果该代码延长至第三页,理解它的难度就大约是原来的四倍。

     

    还有一个可恶的做法就是,在源代码文件的首部或尾部以注释的方式插入变更履历:

     

    /* 6/17/02 SCD fixed the gaforniflat bug */

     

    这到底是有用的信息,还是其维护人员吹的牛?该注释没有任何用处,无论插入它的这一两周以来发生了什么,但它会可怕的赖在那里多年,影响一代又一代的维护人员。一个好得多的替代方案是将这些注解工作留给你的版本控制软件去做;C++源代码文件不是存放条目清单的地方。

     

    要避免不必要的注释,并使代码清晰且易维护,最好的方法之一就是遵循简单、定义良好的命名规约,并选择明晰的名称,使其能够反映所指代实体(函数、类别、变量,等等)的抽象含义。在声明中使用规整的参数名称特别重要。考虑下面接收三个可辨识型别之参数的函数:

     

    /*

     Perform the action from the source to the destination.

     Arg1 is action code, arg2 is source, and arg3 is destination.

     由源位置到目标位置实施操作。

     第一个参数是操作代码,第二个参数是操作源,第三个参数是操作目标。

    */

    void perform( int, int, int );

     

    不算太糟,但想想如果函数有七八个参数而不是三个的时候会怎么样呢。我们可以做得更好:

     

    void perform( int actionCode, int source, int destination );

     

    这样好些,尽管我们可能还是会遇到一个爱打趣的人来告诉我们该函数会做什么(尽管不是告诉我们函数怎么做)。声明中规整的参数名称最吸引人的一个地方在于它不同于注释,它通常能够随其余的代码被一同维护,尽管它对代码的含义没有任何影响。我无法想象出单独一个程序员如何能够在没有同时改变相应参数名称的情况下将第二个和第三个参数的含义弄反,但我可以确定,有众多的程序员会在没有维护相应注释的情况下对调两者的含义。

     

    或许Kathy StarkProgramming in C++中说得最好:“如果能在程序中使用有意义且助记的名称,那么就只会在偶尔的情况下才需要额外的注释。而如果没有使用有意义的名称,那么附加的注释也不会使代码更容易理解。”

     

    另一个将注释最少化的方法是,采用标准的或者众所周知的组件:

     

    printf( "Hello, World!" ); // 列印 "Hello, World" 到屏幕上

     

    这段注释即没用,也很难保持正确性。关键不在于标准组件必需是自文档化的(self-documenting),而在于它们早就被妥当归档并且众所周知了。

     

    swap( a, a+1 );

    sort( a, a+max );

    copy( a, a+max, ostream_iterator<T>(cout,"/n") );

     

    swap,sort以及copy是标准的组件,因此插入额外的注释只会扰乱源代码,并引入对标准操作的不精确描述。

     

    注释并不是天生就有害的?注释经常是必要的?但它们必须被维护,而且它们总是比所评注的代码更难维护。注释不应该表述显而易见的事情,也不应该提供那种能在其它地方做更佳维护的信息。我们的目标并非不择手段的消去注释,而是要采用最少量的注释让代码变得更容易理解和维护。

     

     

    Gotcha条款17Maximal Munch问题

     

    如果遇到这样的表达式你怎么办:

     

    ++++p->*mp

     

    你是否处理过“Sergeant operator[译注1]

     

    template <typename T>

    class R {

      // . . .

      friend ostream &operator <<< // a sergeant operator?

        T >( ostream &, const R & );

    };

     

    你有没有考虑过下面表达式的合法性?

     

    a+++++b

     

    欢迎来到maximal munch [译注2]的世界。在C++编译的前期阶段中,编译器里施行“词法分析(lexical analysis)”的模块[译注:即词法分析器]的任务是,将输入流断开成为“words(单字)”或称token(字元)。当遇到像 ->* 这样的字符序列时,词法分析器可以将其辨识成三个token-,>以及*),或者两个token->*),或者一个token->*)。为了避免这种含混不明的状况,词法分析器总是辨识最长的token,尽可能多的读过其所能合法读取的字符——这就是maximal munch

     

    表达式 a+++++b 是不合法的,因为其被分解为 a ++ ++ + b,对 a++ 这样的 rvalue 施以 post-increment(后增操作)是不合法的。如果你本来是想先对apost-increment(后增操作),再把其结果加上对b pre-increment(先增操作)的结果,那么你必须在表达式中加入至少一个空格:a+++ ++b。如果你对阅读你代码的读者还有那么一点点尊重,你就得再引入另一个空格,尽管其并不必要:a++ + ++b;另外,也没有人会责怪你再加入几个圆括弧:(a++) + (++b)

     

    Maximal munch 所解决的问题比它所产生的问题要多,但在两个常见的情况下,它有些恼人。第一种情况是,templates实体化的时候,其所带的参数本身又是被实体化的templates。例如,一个人可能希望运用标准程序库来声明一个string vector list

     

    list<vector<string>> lovos; // 错的!

     

    不幸的是,实体化表达式中的两个邻接的右尖括号被解释成一个移位操作符,我们会遇到语法错误。这里需要增加空格:

     

    list< vector<string> > lovos;

     

    第二种情况涉及到针对pointer formal arguments运用 default argument initializers

     

    void process( const char *= 0 ); // error!

     

    这个声明企图在formal argument declaration中使用赋值运算符 *=。这是个语法错误。其属于“wages of sin”一类问题,即是说:如果代码的作者给这个formal argument一个名称的话,就不会出现错误。这个名称不仅可以为自身提供最佳的文档化说明,也杜绝了maximal munch问题:

     

    void process( const char *processId = 0 );

     

    关于作者

    Stephen C. Dewhurst (<www.semantics.org>) 是坐落于Massachusetts东南部酸果泽中的Semantics Consulting, Inc.的总裁。他专精于C++咨询,高阶C++编程、STL和设计模式的培训。Steve还是The C++ Seminar的主要讲师之一。

    注解

    [译注1]Sergeant operator(军士操作符),这是一个很诙谐的说法,因为代码中本来是一个template function的声明,结果排列出来的时候,operator << template所带的 < 连起来形成 operator <<<,活象个军衔标志。

    [译注2]maximal munch,最大化吞取。

     

    S. Dewhurst, C++ Gotchas (#1 and #17). c 2003 Pearson Education, Inc. All rights reserved.


    最新回复(0)