《C++面向对象高效编程(第2版)》——2.21 确保抽象的可靠性——类不变式和断言

简介:

本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第2章,第2.21节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.21 确保抽象的可靠性——类不变式和断言

C++面向对象高效编程(第2版)
任何抽象都必须与客户履行它的契约(contract)。当客户使用类时,他希望类的对象像其发布描述的那样运行正常。另一方面,类的实现者必须千方百计地确保对象运行正常。但是,类只有在客户履行自己那部分契约后,才能正确行使它的职责。例如,类的成员函数可能要求传入的参数为非零指针(non-zero pointer)。只有满足此前提条件,成员函数才能保证它的行为。因此,客户必须履行一些义务。换言之,如果客户履行了她那部分契约,对象的实现者必须尊重客户那部分契约,并确保正当的行为。

在类和成员函数的文档中说明契约,这个主意不错。但是,把这些契约条件作为类的一部分实现代码,在执行类实现时检测契约条件,效果更好。这体现了断言(assertion)的巨大价值所在。

通常,断言是一个用于评估真假的表达式。如果表达式评估为假,则断言失败。例如,在TlaserDiscPlayer类中,Play成员函数会包含一个托盘关闭的断言。稍后即将介绍它的语法。

2.21.1 类不变式

进一步研究发现,在每个成员函数中(或甚至在一个成员函数的内部的多处)都包含一个断言可能并不方便。每个类都会在对象中包含一些恒为真的条件,无论对象调用任何成员函数,这些条件都必须为真。这样的条件称为类不变式(class invariant)。顾名思义,在对象中这些条件恒为真。如果我们以某种方式给类添加这些条件,并保证每个成员函数的代码都检查这些条件,将会非常方便。

2.21.2 前置条件和后置条件

除这些类不变式之外,成员函数可能会包含其他条件,在执行代码前必须保证这些条件为真。这些在操作开始被调用之前必须为真的条件,称为前置条件(precondition)。

C++:

在C和C++中,断言已经使用很长一段时间。所有的C和C++编译器都支持assert宏。该宏接受一个表达式,而且必须判断表达式的真假。倘若表达式判断为真,则继续执行;倘若表达式为假,则程序停止,并显示错误消息表明断言失败。消息中包含文件名,违规的语句源代码行号。这是最简单的(且唯一可用的)断言形式。Play成员函数的断言代码如下:

TLaserDiscPlayer::Play(unsigned atChapter)
{
  // 这是断言代码
  assert(this->_trayStatus == eClosed);
  // 略去其余代码
}```
Play成员函数在被调用之前,要求影碟播放机托盘关闭,这个条件表示为断言。
一旦成员函数完成它的操作,将会执行某些条件必须为真的断言。换言之,如果成员函数成功地执行完毕,它将生成一个满足某些条件的结果,这样的条件被称为后置条件(postcondition)。

Eiffel:

在Eiffel中,前置条件和后置条件都是非常流行的概念,而且得到了很好的支持。Eiffel运行期系统检查这些条件,并确保它们为真,否则,停止正在运行的程序。在require子句中,前置条件置于操作的开始。操作的后置条件将在操作末尾的ensure子句中说明。按照这样的方式,每个操作不管在何处被调用,都可以自由地使用任何前提和后置条件。

在进入和退出每个操作(成员函数)时,都必须检查类不变式。为了让实现者便于使用它,我们最好能将所有的不变式都置于类内部的已知区域。类不变式就是在对象的生命期内,必须保证对象状态的语句。例如,Person抽象中可能包含某个不变式,用于保证人的出生日期必须为有效数据,且姓名正确。与此类似,不允许透支的BankAccount抽象中可能包含余额不能小于零的不变式。这些不变式功能强大,它们允许实现者清楚地规定类的某些特性。客户在使用类的对象时,可以认为这些条件为真。

C++和Smalltalk都没有内置支持类不变式,但是Eiffel内置支持类不变式。

Eiffel:

在Eiffel中,类不变式定义在类内部的invariant区,每个操作内部都会检查它们。特别是,在进入和退出每个操作时,会检查类不变式是否为真。这确保可以随时预测类对象的行为。实际上,操作的每个前置条件和后置条件都包含对类不变式的判断。

###2.21.3 使用断言实现不变式和条件
既然C++未直接支持类不变式,我们就必须设法构造一些策略来达到类似的效果。assert宏可以在这派上用场,以下就是一个简单的策略。

简单地定义一对宏:PRE_CONDITION和POST_CONDITION,它们使用assert宏。

define PRE_CONDITION(condition) assert(condition)

define POST_CONDITION(condition) assert(condition)`

如果需要,可以在这些宏中添加消息,当断言失败时打印消息。

#define PRE_CONDITION(message, condition) assert( (message, (condition)))
#define POST_CONDITION(message, condition) assert( (message, (condition)))```
当断言失败,希望打印失败消息时,这会有所帮助。例如,你可以编写以下代码:

`PRE_CONDITION(“Laser disc tray is not closed”, (_trayStatus==eClosed));`
虽然这样的前置条件和后置条件只是简单的断言,但是,它们让程序更可靠、更易读,而且更加易于理解。

然而,添加对类不变式的支持并不容易。记住,在每次进入和退出每个成员函数时,类不变式都必须为真。要保证这一点,需要在一个函数(名为InvariantChecker)中定义一组条件,且在每次进入和退出每个成员函数时,都调用该函数。这样的方法冗长且易出错,因为每个成员函数必须要调用这个函数。如果在类中添加新函数,这样的方法甚至更易出错。但是,却没有其他可行的办法,有总比没有好。如果没有其他办法,至少要在文档中清楚地说明,用户才能明确地知道实现者所保证的契约是什么。很多类的设计者提供非常清楚的类不变式文档,并遵循一些策略确保不变式被强制执行。

注意:
当使用任何一种策略时,都要根据需要添加可关闭或开启策略的支持(一对`#ifdef`)。
###2.21.4 高效使用断言
高效使用断言可实现更可靠的程序。当条件不成立时,断言至少保证程序不会继续执行。但是发生断言失败,就不可能再复原。要解决这个问题,需要用到C++支持的真正的异常管理工具(参见第10章)。但是,理解和使用异常并不容易,它需要适当的架构和高效的设计。断言简单且易于实现,众多程序员已经使用多年。

hand即使你并不打算精心设计程序,但至少要练习使用简单的断言。

不要认为一旦开始使用断言,就不能转用其他更好的处理方式。如果将来决定使用真正的异常管理,只需在代码中找到所有调用assert的地方,然后用适当的throw语句替换即可。
相关文章
|
7天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
22 2
|
13天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
41 5
|
19天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
49 4
|
21天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
47 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
17 0
|
2月前
|
存储 编译器 C语言
深入计算机语言之C++:类与对象(上)
深入计算机语言之C++:类与对象(上)