《Imperfect C++中文版》——1.3 运行期契约:前置条件、后置条件和不变式

本节书摘来自异步社区出版社《Imperfect C++中文版》一书中的第1章,第1.3节,作者: 【美】Matthew Wilson,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.3 运行期契约:前置条件、后置条件和不变式

Imperfect C++中文版
“如果例程的所有前置条件(precondition)已经被调用者满足了,那么该例程必须确保当它完成时所有后置条件(postconditions)(以及任何不变式)皆为真。”——Hunt and Thomas, The Pragmatic Programmers [Hunt2000]。

如果我们无法执行编译期强制,那么还可以采用运行期强制。运行期强制的一个系统化的实现途径是指定函数契约。函数契约精确定义了在函数被调用之前调用者必须满足哪些条件(前置条件),以及在函数返回之时哪些条件(后置条件)是调用者可以期望的。契约的定义以及它们的强制实施是DbC(Design by Contract,契约式设计)[Meye1997]的基石。

前置条件是指函数履行其契约所必须满足的条件。满足前置条件是调用者的责任,而被调用者则假定它的前置条件已经被满足,并且仅当它的前置条件被满足时才负责提供正确的行为。这一点非常重要,在[Meye 1997]中被强调指出。倘若调用者没有满足前置条件,则被调用者做出任何事情都是完全合理的。事实上,通常这会引发一个断言(见1.4节),进而可能导致程序终止。这听起来似乎颇令人恐慌,刚接触DbC的程序员通常会对此感到很不舒服,直到你问起他们:如果一个函数的(前置)条件都不能被满足,那还能指望它有什么样的行为时,他们才哑口无言。事实上,契约越严格,违反它所导致的后果越严重,从而软件的质量就会越好。当转到DbC上时,要理解这一点是最为困难的。

后置条件在函数执行完毕时必须为真。确保后置条件被满足是被调用者的责任。当函数返回控制时调用者可以假定后置条件已经得到了满足。在现实中,有些时候有所保留(不要把赌注全部押在被调用者身上)还是必要的,例如,当调用应用服务器中的第三方插件时就是如此。然而,我认为前面所讲的原则仍然是对的。事实上,对违反契约的插件的合理反应之一是将它卸载掉,并给公司经理以及第三方插件厂商发一封电子邮件。既然我们对于违反契约的行为可以作出任何反应,那么有什么理由不这么做呢?

前置条件和后置条件可以被应用到类的成员函数,也可以被用到自由函数身上,这对于C++(更一般地说,面向对象编程)来说很有益处。事实上,还有另外一个与DbC相关的东西,它只能依附于类而存在,那就是类不变式(class invariant)。类不变式是指一个或一组条件式,它们对于一个处于良好定义状态的对象总是为真。根据定义,类的构造函数负责确保类的实例进入一个符合该类的不变式的状态中,而类的(public)成员函数则在它们完成之际确保类的实例仍然处在该状态中。仅当处于构造函数、析构函数或其他某个成员函数的执行过程中时,类不变式才不一定要为真。

在某些场合下,将不变式的作用范围定义为比“单个对象的状态”的范围更广可能更合适一些。原则上,不变式可以被应用到操作环境的整个状态上,然而,在实践中,这种情况是极其少见的,类不变式则很常见。因此,在本章以及本书剩余的篇幅中,如果提到不变式,均是指类不变式。

对部分或根本没有进行封装的类型提供不变式是可行的(见3.2节和4.4.1小节),这个不变式是由与该类型相关的API函数(以及该函数的前置条件)来强制实施的。事实上,当使用这种类型时,不变式是极好的主意,因为它们缺乏封装性的特质提高了滥用的风险。不过这种不变式相当容易被“绕过”,这也说明了为什么通常应该避免使用这种类型。事实上,[Stro2003]中某种程度上提到:如果存在一个不变式,则公有数据简直毫无意义。封装既是关于隐藏实现又是关于保护不变式的。至于“属性”(第35章),可能是为了结构上的一致性(见20.9节)而引入的,只不过为我们提供公有成员变量的表象而已,它仍然具有不变式。

对于违反前置条件、后置条件或者不变式,你所采取的行动完全由你来决定。你可以把信息记录到日志文件中,也可以抛出异常,或者给你家人发一封SMS,告诉她今夜你将debug到很晚。不过,通常我们采取的行动是引发一个断言。

1.3.1 前置条件

在C++中,前置条件测试相当简单。在这本书中我们已经看到了好几个例子。它和使用断言一样简单:

template< . . . >
typename pod_vector<. . .>::reference pod_vector<. . .>::front()
{MESSAGE_ASSERT("Vector is empty!", 0 != size());assert(is_valid());return m_buffer.data()[0];
}

1.3.2 后置条件

这是C++容易产生磕磕碰碰的地方。这里的挑战是在函数的退出点捕获返回值和“输出”参数。1当然了,C++提供了特别有用的RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制(见3.5节),该机制保证当执行流程退出某个作用域时栈上对象的析构函数都会得到调用。这就意味着我们可能借助这一点实现一个可行方案,至少该机制具备这个潜力。

我们的选择之一是声明监视器对象,它持有对输出参数和返回值的引用。

int f(char const *name, Value **ppVal, size_t *pLen)
{int                 retVal;retval_monitor    rvm(retVal, . . . policy . . . );outparam_monitor  opm1(ppVal, . . . policy . . . );outparam_monitor  opm2(pLen, . . . policy . . . );. . . // 函数体return retVal;
}

一些策略会被用来检查变量是否为NULL,或者是否位于一个特定的区间内,或者是一组数值中的一个,等等。尽管实现这些东西都有困难,这里仍然存在两个问题。第一,rvm的析构函数会对它所持有的指向函数返回值变量retVal的引用来施行约束。如果函数的其他任何部分返回了一个不同的值(或一个常量),那么rvm无可避免地会报告一次失败。为了能够正确工作,我们不得不强制让所有函数都通过单个变量来返回,这肯定不符合一些人的口味,在某些场合下也是不可能的。

然而,最主要的问题还在于各个后置条件监视器之间是没有关联的。大多数函数的后置条件是复合型的,个体输出参数和返回值仅当符合某种一致的关系时才有意义,例如:

assert(retVal > 0 || (NULL == *ppVal && 0 == *pLen));

我不打算建议你如何将这3个个体监视器对象以这样的方式结合起来,以便强制实施各种各样的后置条件状态,这类事情对于模板元编程爱好者可能是一个令人激动的挑战,不过对于其他人,它所带来的复杂性不值得我们付出代价。

Imperfection: C++对后置条件未提供合适的支持。
在我看来,惟一合理的(虽然看起来很平凡)解决方案是,通过一个转发函数将(待调用)函数和对它的(后置条件)检查分离开来,就像在程序清单1.5中展示的那样:

程序清单1.5

int f(char const *name, Value **ppVal, size_t *pLen)
{. . . // 进行f()的前置条件检查int retVal = f_unchecked(name, ppVal, pLen);. . . // 进行f()的后置条件检查return retVal;
}
int f_unchecked(char const *name, Value **ppVal, size_t *pLen)
{. . . // f的语义
}

在实际代码中,你可能希望在不需要执行DbC的地方省略掉所有的检查,为此我们需要使用预处理器:

程序清单1.6

int f(char const *name, Value **ppVal, size_t *pLen)
#ifdef ACMELIB_DBC
{. . . // 进行f()的前置条件检查int retVal = f_unchecked(name, ppVal, pLen);. . . // 进行f()的后置条件检查return retVal;
}
int f_unchecked(char const *name, Value **ppVal, size_t *pLen)
#endif /* ACMELIB_DBC */
{. . . // f的语义
}

这完全算不上优雅,不过它可以工作,并可以很容易地合并到代码生成器中。当处理被重写的(overridden)类成员函数时,问题可能要稍微复杂一点,因为你要面对是否实施父类的前置条件和后置条件的问题。这得条分缕析后才能决定,已经超出了我们的讨论范围。2

1.3.3 类不变式

在C++中,实现类不变式几乎和实现前置条件一样简单。我个人的做法是为类定义一个名为is_valid()的方法,像这样:

template<. . . >
inline bool pod_vector<. . .>::is_valid() const
{if(m_buffer.size() < m_cItems){return false;}. . . // 这里进行进一步的检查return true;
}

然后,该类的每个公有方法都把它放在断言里进行调用,在进入方法时断言一次,退出方法前再来一次。我喜欢在紧接着前置条件检查之后进行类不变式的检查(见1.3.1小节):

template< . . . >
inline void pod_vector<. . .>::clear()
{assert(is_valid());m_buffer.resize(0);m_cItems = 0;assert(is_valid());
}

作为一种替代策略,我们可以将断言放在不变式函数自身之中。然而,除非你手头拥有的是一个“久经考验”的断言(见1.4节),否则这会令你不得不选择提供关于“肇事”的条件或方法的断言信息(文件+行+消息)。我倾向于后者,因为违反不变式毕竟是非常少见的情况。不过,你可能会选择前者,如果是那样的话,你可能希望将断言放到is_valid()成员函数中。

事实上,对此存在一个合理的折中方案,我通常在具有良好的日志/跟踪界面的环境中使用这种策略(见21.2节),具体做法是在is_valid()成员函数中记录违反不变式的细节,并且让“肇事”成员函数3来触发该断言。

与输出参数和返回值检查不同,使用RAII(见3.5节)来使类不变式的检查自动化还是相当容易的(这种检查也作为方法退出前的后置条件验证的一部分),像这样:

template< . . . >
inline void pod_vector<. . .>::clear()
{check_invariant<class_type> check(this);m_buffer.resize(0);m_cItems = 0;
}

缺点是,强制会在check_invariant模板实例的构造函数和析构函数中被实施,这意味着使用预处理器来获悉  FILE 和 LINE 信息的简单的断言可能会给出误导信息。然而,要想实现一个可以正确显示断言失败位置的“宏+模板”的断言形式并不算是很大的挑战,甚至可以结合运用非标准的 FUNCTION 预处理符号(当然,对于那些支持它的编译器而言)。

1.3.4 检查?总是进行

在[Stro2003]中,Bjarne Stroustrup做了一个非常重要的观察:不变式只对那些具有方法的类才是必要的,而对于仅仅作为变量聚合体的简单结构而言是没有必要的(例如,我们将会在4.4.2小节看到的Patron类型就不需要不变式)。在我看来,这话还可以这么说:任何具有方法的类都应该具有类不变式。不过,在实践中对此有一个下限。如果你的类持有一个指向某些资源的指针,那么,它要么是NULL,要么不是NULL。除非你的类不变式方法可以使用非空指针所指向的有效的外部资源,否则你的类不变式将无事可干。在这种情况下,是否使用一个“存根(stub)”类不变式取决于你自己,或者你也可以干脆什么都不干。但如果你的类将来会不断升级,那么在里面放上一块有待以后扩充的“存根”方法可以令后续的精化工作变得容易一些。如果你使用了某种代码生成器的话,我建议你总是用它来生成类不变式,并生成对所生成的类不变式的调用。

类不变式较之散落在类实现周围的一堆断言而言,好处是非常明显的。类不变式使你的代码更容易阅读,并且在不同的类的实现之间具有一致的外观,以及具有更好的可维护性,这是因为对于每个类你都把类不变式定义在了某个单一的地方。

1.3.5 DbC还是不DbC

到目前为止,我所描绘的关于运行期契约的蓝图其实隐含了一个假定,那就是:在进行适当的测试后,人们会对他们的系统进行一次构建(build),4在这次构建中,DbC元素都被预处理器消去。5

事实上,关于“是否任何构建(build)都应该不实施DbC”这个问题[Same2003],仍然颇有争议。一个论据是(借用[Same2003]里的逻辑)DbC里的契约实施就好比电力系统中的保险丝,任何人都不应该在部署一个成熟的电力设备之前把它里面的所有保险丝都拔掉。

断言和保险丝之间的区别在于前者涉及运行期测试,而测试的代价明显不为零。尽管保险丝中的合金成分的电阻可能与它所在系统中的其他部分的电阻略有差别,然而这跟断言引入的代价相比仍然无法相提并论。我的看法是,这需要仔细分析才能求得一个良好的平衡。这就是为什么本节的例子代码中包含了ACMELIB_DB这个符号的缘故。我没有使用NDEBUG(或者_DEBUG),因为DbC的使用不应该直接和“调试版/发行版(debug/release)”的二进制概念耦合起来。究竟何时使用它,何时消除它,取决于你自己。6

1.3.6 运行期契约:尾声

尽管我们已经看到C++在后置条件方面是有缺陷的,然而进行前置条件和类不变式的测试仍然是合理的。在实践中,将这两者结合使用往往能发挥DbC大部分的威力。对返回值和输出参数的后置条件测试的能力缺失虽然令人遗憾,但也并非十分严重的事情。如果你必需这种能力的话,你可以求助于预处理器,就像在1.3.2小节中看到的那样。

如同约束一样, 对于不变式,我们可以通过使用一个间接层让日子好过一些。这个间接层对于约束来说是一个宏,而对于不变式来说则是一个成员函数。正因为如此,提供对新的编译器的支持或者修改某个类的内部实现也变得更为容易了,并且,我们还把该机制不爽的那一面全部隐藏到了类不变式方法中。

1译者注:即用于向外界返回东西的函数参数,例如指向待填充的缓冲区的指针。
2在这一点上,我承认我有点胆小自私,不过我有很好的借口。即便是在成熟运用DbC的语言中,对于继承体系中的层与层之间的关联契约的用处(事实上是机制)仍然是模棱两可的。此外,为C++加入DbC的提议直到本书的撰写时仍然不过是纳入考虑而已[Otto2004],因此,我认为在这里过多地在细节上饶舌没有什么好处。
3译者注:而非不变式函数。
4译者注:对程序进行编译和连接的过程。
5译者注:其实通常就是发行版(release)的构建,其中assert(exp)会展开为空。
6在ISE Eiffel 4.5中,你无法去掉前置条件,大概是因为前置条件可以在程序变成未定义状态之前进行反馈,从而对于程序捕获违反前置条件的异常并继续执行是有意义的。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/541497.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python名称空间与运用域_Python名称空间和作用域讲座,命名,Namespaces,Scopes

Python命名空间(Namespaces)和作用域(Scopes)讲座命名空间(Namespace)命名空间(Namespace)&#xff0c;是名称到对象的映射。命名空间提供了在项目中避免名字冲突的一种方法。命名空间是独立的&#xff0c;没有任何关系的&#xff0c;所以一个命名空间中不能有重名&#xff0c;…

getminimum_Java Calendar getMinimum()方法与示例

getminimumCalendar类的getMinimum()方法 (Calendar Class getMinimum() method) getMinimum() method is available in java.util package. getMinimum()方法在java.util包中可用。 getMinimum() method is used to get the minimum value of the given field(fi) of this Cal…

《Spark核心技术与高级应用》——3.2节构建Spark的开发环境

本节书摘来自华章社区《Spark核心技术与高级应用》一书中的第3章&#xff0c;第3.2节构建Spark的开发环境&#xff0c;作者于俊 向海 代其锋 马海平&#xff0c;更多章节内容可以访问云栖社区“华章社区”公众号查看 3.2 构建Spark的开发环境无论Windows或Linux操作系统&am…

python闭包怎么理解_Python 闭包的理解

Last Updated on 2019年10月15日Python中的闭包是一个比较模糊的概念&#xff0c;不太好理解&#xff0c;我最近的面试中也被问及&#xff0c;在一个单例模式的实现上&#xff0c;我用装饰器实现单例&#xff0c;然后面试官就问到了我对闭包的理解&#xff0c;回答的不太清楚。…

Java BufferedReader mark()方法与示例

BufferedReader类mark()方法 (BufferedReader Class mark() method) mark() method is available in java.io package. mark()方法在java.io包中可用。 mark() method is used to mark the current position in this stream and whenever we call reset() method so it will re…

《全球互联网金融商业模式:格局与发展》——第3章,第3节互联网保险公司...

本节书摘来自华章出版社《全球互联网金融商业模式&#xff1a;格局与发展》一书中的第3章&#xff0c;第3.3节互联网保险公司&#xff0c;作者廖理&#xff0c;更多章节内容可以访问云栖社区“华章计算机”公众号查看 3.3 互联网保险公司互联网思维贯穿整个保险创新发展过程&a…

webapi随机调用_BeetleX之webapi验证插件JWT集成

对于webapi服务应用很多时候需要制订访问限制&#xff0c;在前面的章节也讲述了组件如何制订控制器访问控制&#xff1b;但到了实际应用要自己去编写还是比较麻烦。为了让访问控制更方便组件实现基于JWT的控制器访问控制组件BeetleX.FastHttpApi.Jwt&#xff1b;通过这个组件可…

java bitset_Java BitSet nextClearBit()方法与示例

java bitsetBitSet类nextClearBit()方法 (BitSet Class nextClearBit() method) nextClearBit() method is available in java.util package. nextClearBit()方法在java.util包中可用。 nextClearBit() method is used to retrieve the index of the first bit that is set to …

《驯狮记——Mac OS X 10.8 Mountain Lion使用手册》——2.3 Dock

本节书摘来自异步社区《驯狮记——Mac OS X 10.8 Mountain Lion使用手册》一书中的第2章&#xff0c;第2.3节&#xff0c;作者&#xff1a;陈明 , 张铮 , 马玉龙著&#xff0c;更多章节内容可以访问云栖社区“异步社区”公众号查看 2.3 Dock 驯狮记——Mac OS X 10.8 Mountain…

mysql 嵌套if标签_对比Excel、MySQL、Python,分别讲述 “if函数” 的使用原理!

作者&#xff1a;黄伟呢本文转自&#xff1a;数据分析与统计学之美其实&#xff0c;不管是Excel、MySQL&#xff0c;还是Python&#xff0c;“if”条件判断都起着很重要的作用。今天这篇文章&#xff0c;就带着大家盘点一下&#xff0c;这三种语言如何分别使用 “if函数” 。if…

Java BigDecimal intValue()方法与示例

BigDecimal类的intValue()方法 (BigDecimal Class intValue() method) intValue() method is available in java.math package. intValue()方法在java.math包中可用。 intValue() method is used to convert a BigDecimal to an integer and when the converted BigDecimal val…

R语言数据挖掘

数据分析与决策技术丛书 R语言数据挖掘 Learning Data Mining with R &#xff3b;哈萨克斯坦&#xff3d;贝特麦克哈贝尔&#xff08;Bater Makhabel&#xff09; 著 李洪成 许金炜 段力辉 译 图书在版编目&#xff08;CIP&#xff09;数据 R语言数据挖掘 / &#xff08;哈…

linux adduser mysql_linux_adduser

新帐号建立当不加-D参数,useradd指令使用命令列来指定新帐号的设定值and使用系统上的预设值.新使用者帐号将产生一些系统档案&#xff0c;使用者目录建立&#xff0c;拷备起始档案等&#xff0c;这些均可以利用命令列选项指定。此版本为RedHatLinux提供&#xff0c;可帮每个新加…

java iterator_Java ArrayDeque iterator()方法与示例

java iteratorArrayDeque类iterator()方法 (ArrayDeque Class iterator() method) iterator() Method is available in java.lang package. iterator()方法在java.lang包中可用。 iterator() Method is used to return an iterator over the deque elements. iterator()方法用于…

《jQuery、jQuery UI及jQuery Mobile技巧与示例》——7.4 示例:使用按钮集装饰单选框...

本节书摘来自异步社区《jQuery、jQuery UI及jQuery Mobile技巧与示例》一书中的第7章&#xff0c;第7.4节&#xff0c;作者&#xff1a;【荷】Adriaan de Jonge , 【美】Phil Dutson著&#xff0c;更多章节内容可以访问云栖社区“异步社区”公众号查看 7.4 示例&#xff1a;使…

mysql 模拟序列_【原创】MySQL 模拟PostgreSQL generate_series 表函数

PostgreSQL 提供了一个很强大的造数据的函数generate_series&#xff0c;基于Common Table Expression。MySQL 没有复杂的应用程序类型&#xff0c;该如何实现这样的功能呢&#xff1f; 我想到的三种方法如下:1. 用存储过程来做。 缺点是写好多数据库不擅长的应用逻辑。2. 我们…

Python字符串| isdigit()方法与示例

isdigit() is an in-built method in Python, which is used to check whether a string contains only digits or not. isdigit()是Python中的内置方法&#xff0c;用于检查字符串是否仅包含数字。 Digit value contains all decimal characters and other digits which may …

vue2.0的学习

vue-router 除了使用 <router-link> 创建 a 标签来定义导航链接&#xff0c;我们还可以借助 router 的实例方法&#xff0c;通过编写代码来实现。 1&#xff09;router.push(location) 这个方法会向 history 栈添加一个新的记录&#xff0c;所以&#xff0c;当用户点击浏…

mysql+url的配置参数详解_MySql链接url参数详解

mysql URL格式如下&#xff1a;jdbc:mysql://[host:port],[host:port].../[database][?参数名1][参数值1][&参数名2][参数值2]...MySQL在高版本需要指明是否进行SSL连接 在url后面加上 useSSLtrue 不然写程序会有warning常用的几个较为重要的参数&#xff1a;参数名…

Java LocalDate类| minus()方法与示例

LocalDate类isSupported()方法 (LocalDate Class isSupported() method) Syntax: 句法&#xff1a; public LocalDate minus(TemporalAmount t_amt);public LocalDate minus(long amt, TemporalUnit t_unit);isSupported() method is available in java.time package. isSuppo…