现在的位置: 首页 > 综合 > 正文

Enhanced Assertions铪铪铪铪

2013年09月06日 ⁄ 综合 ⁄ 共 6188字 ⁄ 字号 评论关闭

                                Enhanced Assertions

                                        --By Andrei Alexandrescu and John Torjo

                                        刘未鹏(pongba)译

 

    --这篇与John Torjo合著的文章描述了一个特性完备的,工业强度的assertion设施。这个工具包的特性包括多重debug级别,日志记录,还有一个搜集详细状态信息的方法。

 

    好吧,我承认:我正在体验"Writer's block"。材料都在这儿,很酷。我享用了我最喜爱的早餐(自制的牛奶什锦早餐,我的私人配方:50%燕麦片,50%坚果,50%葡萄干--试着干掉它们),我所信赖的open source编辑器正在动人的闪烁着(我知道你在想什么:动人的?滑稽透顶),然而,我无法想出一个好的介绍性的开头。为了缓解我的焦虑,亲爱的读者,原谅我在文章的开始就用这些滑稽的话来令你烦恼。然而这是一个meta-programming专栏,不是么?再加上你是个C++程序员,所以你几乎不会介意一些额外的语法。

 

    既然我们已将第一段的问题放至一边,请允许我向你介绍我的朋友John TorjoC++专家,顾问,"Practical C++"的专栏作家(http://buider.com/),我们长期由e-mail来往。John和我合著了这篇文章,描述了经John的改善过的assertion framework

 

    John在读过我的关于assertions的文章[1](其大部分是Jason Shirk和我讨论的结果)后,发现它有所欠缺。更准确的说,他发现他自己需要assertion设施提供更多的特性。这一切是因为我那篇关于assertions的文章和附的源代码使用了Simple World Assumption的环境。这是个非常专业的术语,你可能也可能没有听过它,所以请允许我将它说得详细一点。

 

    Simple World Assumption下,程序员们有正常的工作时间,有合理的进度表。他们有时间并且被鼓励去进行代码评测。因此代码中任何失败的assertions都会在单件和整体的测试中招来一连串的批评。程序员测试并调试代码,确保在全部测试环境中都没有assertion失败,最终在NDEBUG宏被定义的情况下编译并将小而快的可执行文件发送给他们的项目经理,然后项目经理再将它发送到恰当的消费者基群。顺便一提,在Simple World Assumption之下,项目经理被认为有能力帮助程序员完成他们的工作,并且不给他们施加压力和负担。(正如它所表现出来的,Simple World Assumption在现实中并不存在)

 

    在一个更为现实的世界中,程序员得忍受进度表所带来的相当大的压力,于是写单件的测试被取代为直接将没怎么经过测试的程序不负责任地扔给black-box测试组,而后者为bug出现的情况写bug报告。

 

    指出一段会进展至bug出现的事件序列并不总是简单的。在多种情形并存或多线程,事件驱动下的编程会使bug的重现变得相当困难。使用未初始化变量所产生的随机行为,错误的转型,或者缓冲区溢出只是增加了一些调味料而已。咳,我几乎忘记了名目繁多的系统配置,比如被安装的DLL和注册表设置...(曾经写过一个能在你的系统上完美运行却在另一个上面神秘失败的应用程序吗?)

 

    一个对这种情况有所帮助的方法,John说,是设计一个更好的assertion framework来扩展assertion的能力。明确如下:

 

      * 存在assertions的多重级别、设计错误比白纸黑字更明显。在一个极端,拥有最多的checks,而随着软件的成熟只有最少的会失败。在另一个极端,有"低成本,高效用"checks,你可以将它们为专业测试人员、beta版的测试者、有时甚至可以为你的软件的最终用户保留着。我个人并不喜欢让最终用户看到assertion message,但是John令我相信这种情况是很可能发生的。此外,继续往下读,因为失败的check可以有多种方式来报告。

 

    * 仅仅显示消息是不够的,特别是在开发和测试组分开工作的时候。一个日志记录的设施是极其有用的,这样,在某一次运行失败后,开发者就能够看到哪个assertion(或者哪些assertion)失败了。

 

    * 消息的质量有极大的改善,不仅包括有关失败的表达式、文件、行的详细信息,还包括相关变量的值(在程序员的控制之下)

 

    所有这些额外的特性的加入组成了一个工业强度的assertion工具包,在原来的assertion代码之上,John添加了一些在我的文章"Enforcements"中也被用到的技巧,还有许多属于他自己的技巧。我们会在下面详细介绍经改善过的assertion工具包是如何工作的。

 

提供额外的状态信息(Providing Extra State Information)

    当一个assertion失败时,就意味ASSERT表达式被求值为false。然而,你可能常常想要了解到底是哪个关键变量的值导致了assertion的失败。例如,考虑在某个你确信两个string都为空的时刻,你写出如下代码:

 

      string s1,s2;

      ...

      ASSERT(s1.empty()&&s2.empty());

 

    如果这个assertion失败了,你很可能想要知道myString(译注:泛指string对象,如s1s2,下同)里面到底保存了什么,这能提供对它最后一次更新地点的洞察。Johnframework允许你以如下的语法做到这一点:

 

      SMART_ASSERT(s1.empty()&&s2.empty())(s1)(s2);

 

    注意到圆括号的使用提供了一个给ASSERT以额外的参数的方式,并且这是可扩展的,这与ENFORCE[2]类似。(当然,ENFORCE并非是以这种特殊的风格使用operator()的第一个组件)

 

    当这样做的时候,如果assertion失败了,显示并被记录为日志的消息看起来像这样:

 

      Assertion failed in matrix.cpp: 879412

      Expression: 's1.empty()&&s2.empty()'

      Values: s1="Wake up,Neo"

              s2="It's time to reload."

 

    这就是魔法如何工作的。准备接受一些很棘手但却无疑是值得了解的东西吧。(为了能够理解并受用,你需要唤醒你内心深处对宏的热爱)首先,基本的想法是:要获得变量名称和变量的值,你需要使用"stringizing operator #"(译注:一种特殊的操作符,能使其后面的文本变成C/C++字符串形式,如#class 被编译器替换为"class"),在以上的例子中,你需要对myString使用stringizing operator。但是它只能在宏内部使用,并且以下的事实让事情变得很棘手:你需要一个能无限扩展的宏机制--一个被扩展开来的宏仍然可以继续作为一个宏(这样你才能收集更多的变量s3,s4...的信息)。但是我们都知道这种递归式的宏并不能工作。

 

    然而,如果你能在踩下油门的同时打开汽车顶棚并用你的左手举起天线,你就会发现这技巧虽然困难但毕竟是可行的。荣誉属于Paul Mensonides,是他(就我们目前所知)发明了这个技巧。这儿是你所需要做的:

 

    首先,在你的Assert(这个类和我前一篇文章里定义并使用的同名的类很相似)内部添加两个成员变量SMART_ASSERT_ASMART_ASSERT_B。它们的型别是Assert&

 

      class Assert

      {

         ...

       public:

           Assert& SMART_ASSERT_A;

           Assert& SMART_ASSERT_B;

           //whatever member functions

           Assert& print_current_val(bool,const char*);

           ...

       };

 

    确保你将这些成员用*this初始化。所有这些的全部意图是:如果你有一个Assert型别的对象obj,你可以写obj.SMART_ASSERT_Aobj.SMART_ASSERT_B,而它们的行为跟obj自身的行为一模一样,因为它们都是对*this的引用。第二--很酷的技巧即将登场--定义两个宏:SMART_ASSERT_ASMART_ASSERT_B,它们以某种方式递归至另一个,像这样:

 

      #define SMART_ASSERT_A(x) SMART_ASSERT_OP(x,B)

      #define SMART_ASSERT_B(x) SMART_ASSERT_OP(x,A)

      #define SMART_ASSERT_OP(x,next) /

          SMART_ASSERT_A.print_current_val((x),#x).SMART_ASSERT_##next

 

    如你所见,当你调用SMART_ASSERT_A(xyz),它将会被扩展成一段以SMART_ASSERT_B为结尾的代码。当你调用SMART_ASSERT_B(xyz)时,它会被扩展成一段以SMART_ASSERT_A为结尾的代码。在扩展的时候,这两个宏获取你传给它们的值xyz和值的字符串形式(译注:既"xyz",这是通过stringizing operator #实现的)

 

    是的,这真棘手。一个可以帮你理解这个技巧的意见是:当预处理器看到SMART_ASSERT_A(_B)后面跟着一对括号时,它就将这个当成对宏的调用来对待。如果没有括号,预处理器就简单地将这个符号仍然留在那儿。而在后一种情况下,符号SMART_ASSERT_A(_B)只是代表成员变量。这儿是SMART_ASSERT宏定义,它开始了那两个宏(SMART_ASSERT_ASMART_ASSERT_B)的轮番上场的动作:

 

      #define SMART_ASSERT(expr) /

          if( (expr) ) ; /

          else make_assert( #expr).print_context(__FILE__,__LINE__).SMART_ASSERT_A

 

    如果在这个时候你说"Aha!" ,那么你的意见跟我在读了这些代码第二十遍之后的意见一样。好,现在让我们从最初的表达式开始跟踪宏的扩展过程:

 

      SMART_ASSERT(s1.empty()&&s2.empty())(s1)(s2);

 

    让我们首先展开SMART_ASSERT宏。(我们没必要按照预处理器的顺序来展开,为了让过程清晰明了,我们将过程分为几大块。我们还将适当调整展开后的代码的格式)

 

      if( (s1.empty()&&s2.empty()) ) ;

      else make_assert( "s1.empty()&&s2.empty()").

          print_context("matrix.cpp",879412).SMART_ASSERT_A(s1)(s2);

 

    现在让我们再来展开SMART_ASSERT_A宏,以及展开后的出现的SMART_ASSERT_OP宏:

 

      if( (s1.empty()&&s2.empty()) ) ;

      else make_assert( "s1.empty()&&s2.empty()").

          print_context("matrix.cpp",879412).

          SMART_ASSERT_A.print_current_val((s1),"s1").

          SMART_ASSERT_B(s2);

 

    注意SMART_ASSERT_A是如何不再被预处理器作为一个宏来对待的,那是因为它后面并没有紧跟着一个括号(()。扩展SMART_ASSERT_BSMART_ASSERT_OP后的最终结果是:

 

      if ( (s1.empty() && s2.empty()) ) ;

      else make_assert( "s1.empty() && s2.empty()").

          print_context("matrix.cpp", 879412).

          SMART_ASSERT_A.print_current_val((s1), "s1").

          SMART_ASSERT_A.print_current_val((s2), "s2").SMART_ASSERT_A;

 

    考虑到这时SMART_ASSERT_A是被作为成员变量对待的,而且每个成员函数都返回对Assert的引用,这是个完美成型的语句。

 

失败处理和日志记录(Handling and Logging)

    当一个assertion失败时,有两件事情相继发生:

 

      * 失败信息被记录为日志

      * 失败根据它的级别被适当处理

 

    这两个动作是完全直交的,并且能够被分开定制。例如,你能够处于这样一个模式:你从不请求记录错误但是你仍然记录了所有的错误,这在自动运行,push installation(鼓励安装?强制安装?),或者你的"innocent user protection program"(为无知用户提供保护的程序?)方面很有用。

 

    你可以通过将你自己的日志记录(logger)函数传给静态成员函数Assert::set_log(void (*assert_handler)(const assert_context&))以定制日志记录。你可以定义你自己的失败处理例程然后通过调用Assert::set_handler(level,void (*assert_handler)(const assert_context&))将它安置(这与由来以久的set_unexpected的风格一样)assert_context包含了从失败的assertion中获得的上下文(acquired context),这将在下面解释。

 

并非只有一种Assert(Not Just One Type of Assert)

    正如一些老资格的程序员所注意到的,一个应用程序能够有不同的assert级别,其中一些比另一些更紧要。

 

    让我们看一看assert是如何被使用的。典型地,当你假定一些情况永不会发生时你会使用assertion(例如,你期望一个size或一个index变量永远不为负值)

 

抱歉!评论已关闭.