4.设计与声明 Designs and Declarations

所谓软件设计,是“令软件做出你希望它做的事情”的步骤和做法,通常以颇为一般性的构想开始,最终演变成十足的细节,以允许特殊接口(interfaces)的开发。这些接口而后必须转换为 C++声明式。本章中我将对良好 C++接口的设计和声明发起攻势。我以或许最重要、适合任何接口设计的一个准则作为开端:“让接口容易被正确使用,不容易被误用”。这个准则设立了一个舞台,让其他更专精的准则对付一大范围的题目,包括正确性、高效性、封装性、维护性、延展性,以及协议的一致性。

以下准备的材料并不覆盖你需要知道的优良接口设计的每一件事,但它强调某些最重要的考虑,对某些最频繁出现的错误提出警告,为class、function和template设计者经常遭遇的问题提供解答。

条款18:让接口容易被正确使用,不易被误用 Make interfaces easy to use correctly and hard to use incorrectly.

C++在接口之海漂浮。function接口、class接口、template接口……每一种接口都是客户与你的代码互动的手段。假设你面对的是一群“讲道理的人”,那些客户企图把事情做好。他们想要正确使用你的接口。这种情况下如果他们对任何其中一个接口的用法不正确,你至少也得负一部分责任。理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码不该通过编译;如果代码通过了编译,它的作为就该是客户所想要的。

欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。假设你为一个用来表现日期的class设计构造函数:

class Date {
public:
	Date(int month, int day, int year);
	...
};

乍见之下这个接口通情达理(至少在美国如此),但它的客户很容易犯下至少两个错误。第一,他们也许会以错误的次序传递参数:

Date d(30, 3, 1995);	//喔欧!应该是“3,30”而不是“30,3”
第二,他们可能传递一个无效的月份或天数:
Date d(2, 30, 1995);	//喔欧!应该是“3,30”而不是“2,30”

(上个例子也许看起来很蠢,但别忘了,键盘上的2就在3旁边。打岔一个键的情况并不是太罕见。)

许多客户端错误可以因为导入新类型而获得预防。真的,在防范“不值得拥有的代码”上,类型系统(type system)是你的主要同盟国。既然这样,就让我们导入简单的外覆类型(wrapper types)来区别天数、月份和年份,然后于Date构造函数中使用这些类型:

struct Day {
	explicit Day(int d):val(d)
	{}
	int val;
};

struct Month {
	explicit Month(int m):val(m)
	{}
	int val;
};

struct Year {
	explicit Year(int y):val(y)
	{}
	int val;
};

class Date {
public:
	Date(const Month &m, const Day& d, const Year& y);
	...
};

Date d(30, 3, 1995);					//错误!不正确的类型
Date d(Day(30), Month(3), Year(1995));	//错误!不正确的类型
Date d(Month(3), Day(30), Year(1995));	//OK,类型正确

令Day,Month和Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的structs好(见条款22)。但即使structs也已经足够示范:明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。

一旦正确的类型就定位,限制其值有时候是通情达理的。例如一年只有12个有效月份,所以 Month应该反映这一事实。办法之一是利用enum表现月份,但enums不具备我们希望拥有的类型安全性,例如enums可被拿来当一个ints使用(见条款2)。比较安全的解法是预先定义所有有效的Months:

class Month {
public:
	static Month Jan() {return Month(1); }		//函数,返回有效月份。
	static Month Jan() {return Month(2); }		//稍后解释为什么。
	...											//这些是函数而非对象。
	static Month Jan() {return Month(12); }		
	...											//其他成员函数。
private:
	explicit Month(int m);						//阻止生成新的月份
	...											//这是月份专属数据。
};

Date d(Month::Mar(), Day(30), Year(1005));

如果“以函数替换对象,表现某个特定月份”让你觉得诡异,或许是因为你忘记了non-local static对象的初始化次序有可能出问题。建议阅读条款4恢复记忆。

预防客户错误的另一个办法是,限制类型内什么事可做,什么事不能做。常见的限制是加上const。例如条款3曾经说明为什么“以const修饰operator*的返回类型”可阻止客户因“用户自定义类型”而犯错:

if(a * b = c) ... //喔欧,愿意其实是要做一次比较动作。

下面是另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式:“除非有好理由,否则应该尽量令你的types的行为与内置types一致”。客户已经知道像 int这样的type有些什么行为,所以你应该努力让你的types在合样合理的前提下也有相同表现。例如,如果a和b都是ints,那么对a*b赋值并不合法,所以除非你有好的理由与此行为分道扬镳,否则应该让你的types也有相同的表现。是的,一旦怀疑,就请拿ints做范本。

避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。很少有其他性质比得上“一致性”更能导致“接口容易被正确使用”,也很少有其他性质比得上“不一致性”更加剧接口的恶化。STL容器的接口十分一致(虽然不是完美地一致),这使它们非常容易被使用。例如每个 STL 容器都有一个名为 size的成员函数,它会告诉调用者目前容器内有多少对象。与此对比的是Java,它允许你针对数组使用length property,对Strings使用length method,而对Lists使用size method;.NET也一样混乱,其Arrays有个property名为Length,其ArrayLists有个property名为Count。有些开发人员会以为整合开发环境(integrated development environments,IDEs)能使这般不一致性变得不重要,但他们错了。不一致性对开发人员造成的心理和精神上的摩擦与争执,没有任何一个IDE可以完全抹除。

任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事。例如条款 13 导入了一个 factory函数,它返回一个指针指向Investment继承体系内的一个动态分配对象:

Investment* createInvestment();	//来自条款13;为求简化暂略参数。

为避免资源泄漏,createInvestment返回的指针最终必须被删除,但那至少开启了两个客户错误机会:没有删除指针,或删除同一个指针超过一次。

条款 13 表明客户如何将 createInvestment 的返回值存储于一个智能指针如auto_ptr或tr1::shared_ptr内,因而将delete责任推给智能指针。但万一客户忘记使用智能指针怎么办?许多时候,较佳接口的设计原则是先发制人,就令factory函数返回一个智能指针:

std::str1::shared_ptr<Investment> createInvestment();

这便实质上强迫客户将返回值存储于一个 tr1::shared_ptr内,几乎消弭了忘记删除底部Investment对象(当它不再被使用时)的可能性。

实际上,返回 tr1::shared_ptr让接口设计者得以阻止一大群客户犯下资源泄漏的错误,因为就如条款14所言,tr1::shared_ptr允许当智能指针被建立起来时指定一个资源释放函数(所谓删除器,"deleter")绑定于智能指针身上(auto_ptr 就没有这种能耐)。

假设class设计者期许那些“从createInvestment取得Investment*指针”的客户将该指针传递给一个名为 getRidOfInvestment 的函数,而不是直接在它身上动刀(使用delete)。这样一个接口又开启通往另一个客户错误的大门,该错误是“企图使用错误的资源析构机制”(也就是拿 delete 替换 getRidOfInvestment)。createInvestment 的设计者可以针对此问题先发制人:返回一个“将getRidOfInvestment绑定为删除器(deleter)”的tr1::shared_ptr。

tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成 0 时将被调用的“删除器”。这启发我们创建一个 null tr1::shared_ptr 并以getRidOfInvestment作为其删除器,像这样:

//企图创建一个null shared_ptr并携带一个自定的删除器。此式无法通过编译。
std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvestment);

啊呀,这不是有效的 C++。tr1::shared_ptr构造函数坚持其第一参数必须是个指针,而0不是指针,是个int。是的,它可被转换为指针,但在此情况下并不够好,因为tr1::shared_ptr坚持要一个不折不扣的指针。转型(cast)可以解决这个问题:

std::tr1::shared_ptr<Investment> 	//建立一个null shared_ptr并以getRidOfInvestment为删除器
	pInv(static_cast<Investment*>0, //条款27提到static_cast
		getRidOfInvestment);

因此,如果我们要实现 createInvestment使它返回一个 tr1::shared_ptr并夹带getRidOfInvestment函数作为删除器,代码看起来像这样:

std::tr1::shared_ptr<Investment> createInvestment()
{
	std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);	
	retVal = ...;	//令retVal指向正确对象
	return retVal;
}

当然啦,如果被pInv管理的原始指针(raw pointer)可以在建立pInv之前先确定下来,那么“将原始指针传给pInv构造函数”会比“先将pInv初始化为null再对它做一次赋值操作”为佳。至于其原因,请见条款26。

tr1::shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的 "cross-DLL problem"。这个问题发生于“对象在动态连接程序库(DLL)中被 new创建,却在另一个 DLL 内被 delete销毁”。在许多平台上,这一类“跨 DLL 之 new/delete成对运用”会导致运行期错误。tr1::shared_ptr没有这个问题,因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。这意思是……唔……让我举个例子,如果Stock派生自Investment而createInvestment实现如下:

std::tr1::shared_ptr<Investment> createInvestment()
{
	return std::tr1::shared_ptr<Investment>(new Stock);	
}

返回的那个 tr1::shared_ptr可被传递给任何其他 DLLs,无需在意 "cross-DLL problem"。这个指向Stock的tr1::shared_ptrs会追踪记录“当Stock的引用次数变成0时该调用的那个D L L's delete”。

本条款并非特别针对 tr1::shared_ptr,而是为了“让接口容易被正确使用,不容易被误用”而设。但由于 tr1::shared_ptr如此容易消除某些客户错误,值得我们核计其使用成本。最常见的tr1::shared_ptr实现品来自Boost(见条款55)。Boost的shared_ptr是原始指针(raw pointer)的两倍大,以动态分配内存作为簿记用途和“删除器之专属数据”,以 virtual 形式调用删除器,并在多线程程序修改引用次数时蒙受线程同步化(thread synchronization)的额外开销。(只要定义一个预处理器符号就可以关闭多线程支持)。总之,它比原始指针大且慢,而且使用辅助动态内存。在许多应用程序中这些额外的执行成本并不显著,然而其“降低客户错误”的成效却是每个人都看得到。

请记住

■ 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。

■ “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。

■ “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

■ tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes;见条款14)等等。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值