分享
 
 
 

翻译:Effective C++, 3rd Edition, Item 18: 使接口易于正确使用,而难以错误使用

王朝c/c++·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

第四章 设计和声明软件设计——使软件做你想让它做的事情的途径——典型地从相当概括的主意开始,但它们最后成为足够详细的允许开发的详细的接口。这些接口必须能被转化为 C++ 中的声明。在本章,我们着手于设计和声明好的 C++ 接口的问题。我们开始于或许是最重要的设计任何种类的接口的方针:它们应该易于正确使用,而难以错误使用。随着进展,提出许多更为特殊的指导原则分布于范围广阔的主题上,包括正确性,效率,封装,可维护性,可扩展性,和惯例的一致性

后面的章节并不是你需要知道的关于设计好的接口的全部内容,而是采集了一些最重要的注意事项,一些最容易发生的错误的警告,并为类,函数和模板的设计者经常遇到问题给出了解决方案。

Item 18: 使接口易于正确使用,而难以错误使用

C++ 被淹没于接口中。函数接口、类接口、模板接口。每一个接口都意味着客户的代码和你的代码互相影响。假设你在和通情达理的人打交道,那些客户也想做好工作。他们想要正确使用你的接口。在这种情况下,如果他们犯了一个错误,就说明你的接口至少有部分是不完善的。在理想情况下,如果一个接口的一种尝试的用法不符合客户的预期,代码将无法编译,反过来,如果代码可以编译,那么它做的就是客户想要的。

开发易于正确使用,而难以错误使用的接口需要你考虑客户可能造成的各种错误。例如,假设你正在设计一个代表时间的类的构造函数:

class Date {

public:

Date(int month, int day, int year);

...

};

匆匆一看,这个接口似乎是合乎情理的(至少在美国),但是客户可能很容易地造成两种错误。首先,他们可能会以错误的顺序传递参数:

Date d(30, 3, 1995); // Oops! Should be "3, 30" , not "30, 3"

第二,他们可能传递一个非法的代表月或日的数字:

Date d(2, 20, 1995); // Oops! Should be "3, 30" , not "2, 20"

(后面这个例子看上去好像没什么,但是想想键盘上,2 就在 3 的旁边,这种 "off by one" 类型的错误并不罕见。)

很多客户错误都可以通过引入新的类型来预防。确实,类型系统是你阻止那些不合适的代码通过编译的主要支持者。在当前情况下,我们可以引入简单的包装类型来区别日,月和年,并将这些类型用于 Data 的构造函数。

struct Day { struct Month { struct Year {

explicit Day(int d) explicit Month(int m) explicit Year(int y)

:val(d) {} :val(m) {} :val(y){}

int val; int val; int val;

}; }; };

class Date {

public:

Date(const Month& m, const Day& d, const Year& y);

...

};

Date d(30, 3, 1995); // error! wrong types

Date d(Day(30), Month(3), Year(1995)); // error! wrong types

Date d(Month(3), Day(30), Year(1995)); // okay, types are correct

将日,月和年做成封装数据的羽翼丰满的类比上面的简单地使用 struct 更好(参见 Item 22),但是即使是 struct 也足够证明明智地引入新类型在阻止接口的错误使用方面能工作得非常出色。

只要放置了正确的类型,它往往能合理地限制那些类型的值。例如,月仅有 12 个合法值,所以 Month 类型应该反映这一点。做到这一点的一种方法是用一个枚举来表现月,但是枚举不像我们希望的那样是类型安全(type-safe)的。例如,枚举能被作为整数使用(参见 Item 2)。一个安全的解决方案是预先确定合法的 Month 的集合:

class Month {

public:

static Month Jan() { return Month(1); } // functions returning all valid

static Month Feb() { return Month(2); } // Month values; see below for

... // why these are functions, not

static Month Dec() { return Month(12); } // objects

... // other member functions

private:

explicit Month(int m); // prevent creation of new

// Month values

... // month-specific data

};

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

如果用函数代替对象来表现月的主意让你感到惊奇,那可能是因为你忘了非局部静态对象(non-local static objects)的初始化的可靠性是值得怀疑的。Item 4 能唤起你的记忆。

防止可能的客户错误的另一个方法是限制对一个类型能够做的事情。施加限制的一个普通方法就是加上 const。例如,Item 3 解释了使 operator* 的返回类型具有 const 资格是如何能够防止客户对用户自定义类型犯下这样的错误:

if (a * b = c) ... // oops, meant to do a comparison!

实际上,这仅仅是另一条使类型易于正确使用而难以错误使用的普遍方针的一种表现:除非你有很棒的理由,否则就让你的类型的行为与内建类型保持一致。客户已经知道像 int 这样的类型如何表现,所以你应该努力使你的类型的表现无论何时都同样合理。例如,如果 a 和 b 是 int,给 a*b 赋值是非法的。所以除非有一个非常棒理由脱离这种表现,否则,对你的类型来说这样做也应该是非法的。

避免和内建类型毫无理由的不相容的真正原因是为了提供行为一致的接口。很少有特性比一致性更易于引出易于使用的接口,也很少有特性比不一致性更易于引出令人郁闷的接口。STL 容器的接口在很大程度上(虽然并不完美)是一致的,而且这使得它们相当易于使用。例如,每一种 STL 容器都有一个名为 size 的成员函数可以知道容器中有多少对象。与此对比的是 Java,在那里你对数组使用 length 属性,对 String 使用 length 方法,而对 List 却要使用 size 方法,在 .NET 中,Array 有一个名为 Length 的属性,而 ArrayList 却有一个名为 Count 的属性。一些开发人员认为集成开发环境(IDEs)能补偿这些琐细的矛盾,但他们错了。矛盾在开发者工作中强加的精神折磨是任何 IDE 都无法完全消除的。

任何一个要求客户记住某些事情的接口都是有错误使用倾向的,因为客户可能忘记做那些事情。例如,Item 13 介绍了一个 factory 函数,它返回一个指向动态分配的 Investment 继承体系中的对象的指针。

Investment* createInvestment(); // from Item 13; parameters omitted

// for simplicity

为了避免资源泄漏,createInvestment 返回的指针最后必须被删除,但这就为至少两种类型的客户错误创造了机会:删除指针失败,或删除同一个指针一次以上。

Item 13 展示了客户可以怎样将 createInvestment 的返回值存入一个类似 auto_ptr 或 tr1::shared_ptr 智能指针,从而将使用 delete 的职责交给智能指针。但是如果客户忘记使用智能指针呢?在很多情况下,一个更好的接口会预先判定将要出现的问题,从而让 factory 函数在第一现场即返回一个智能指针:

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

这就从根本上强制客户将返回值存入一个 tr1::shared_ptr,几乎完全消除了当底层的 Investment 对象不再使用的时候忘记删除的可能性。

实际上,返回一个 tr1::shared_ptr 使得接口的设计者预防许多其它客户的与资源泄漏相关的错误成为可能,因为,就像 Item 14 解释的:当一个智能指针被创建的时候,tr1::shared_ptr 允许将一个资源释放(resource-release)函数——一个 "deleter" ——绑定到智能指针上。(auto_ptr 则没有这个能力。)

假设从 createInvestment 得到一个 Investment* 指针的客户期望将这个指针传给一个名为 getRidOfInvestment 的函数,而不是对它使用 delete。这样一个接口又为一种新的客户错误打开了门,这就是客户可能使用了错误的资源析构机制(也就是说,用了 delete 而不是 getRidOfInvestment)。createInvestment 的实现可以通过返回一个在它的 deleter 上绑定了 getRidOfInvestment 的 tr1::shared_ptr 来预防这个问题。

tr1::shared_ptr 提供了一个需要两个参数(要被管理的指针和当引用计数变为零时要调用的 deleter)的构造函数。这里展示了创建一个以 getRidOfInvestment 为 deleter 的 null tr1::shared_ptr 的方法:

std::tr1::shared_ptr<Investment> // attempt to create a null

pInv(0, getRidOfInvestment); // shared_ptr with a custom deleter;

// this won't compile

唉,这不是合法的 C++。tr1::shared_ptr 的构造函数坚决要求它的第一个参数应该是一个指针,而 0 不是一个指针,它是一个 int。当然,它能转型为一个指针,但那在当前情况下并不够好用,tr1::shared_ptr 坚决要求一个真正的指针。用强制转型解决这个问题:

std::tr1::shared_ptr<Investment> // create a null shared_ptr with

pInv(static_cast<Investment*>(0), // getRidOfInvestment as its

getRidOfInvestment); // deleter; see Item 27 for info on

// static_cast

据此,实现返回一个以 getRidOfInvestment 作为 deleter 的 tr1::shared_ptr 的 createInvestment 的代码看起来就像这个样子:

std::tr1::shared_ptr<Investment> createInvestment()

{

std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),

getRidOfInvestment);

retVal = ... ; // make retVal point to the

// correct object

return retVal;

}

当然,如果将被 pInv 管理的裸指针可以在创建 pInv 时被确定,最好是将这个裸指针传给 pInv 的构造函数,而不是将 pInv 初始化为 null 然后再赋值给它。至于方法上的细节,参考 Item 26。

tr1::shared_ptr 的一个特别好的特性是它自动逐指针地使用 deleter 以消除另一种潜在的客户错误——“cross-DLL 问题。”这个问题发生在这种情况下:一个对象在一个动态链接库(dynamically linked library (DLL))中通过 new 被创建,在另一个不同的 DLL 中被删除。在许多平台上,这样的 cross-DLL new/delete 对会引起运行时错误。tr1::shared_ptr 可以避免这个问题,因为它的缺省的 deleter 只将 delete 用于这个 tr1::shared_ptr 被创建的 DLL 中。这就意味着,例如,如果 Stock 是一个继承自 Investment 的类,而且 createInvestment 被实现如下,

std::tr1::shared_ptr<Investment> createInvestment()

{

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

}

返回的 tr1::shared_ptr 能在 DLL 之间进行传递,而不必关心 cross-DLL 问题。指向这个 Stock 的 tr1::shared_ptr 将保持对“当这个 Stock 的引用计数变为零的时候,哪一个 DLL 的 delete 应该被使用”的跟踪。

这个 Item 不是关于 tr1::shared_ptr 的——而是关于使接口易于正确使用,而难以错误使用的——但 tr1::shared_ptr 正是这样一个消除某些客户错误的简单方法,值得用一个概述来看看使用它的代价。最通用的 tr1::shared_ptr 实现来自于 Boost(参见 Item 55)。Boost 的 shared_ptr 的大小是裸指针的两倍,将动态分配内存用于簿记和 deleter 专用(deleter-specific)数据,当调用它的 deleter 时使用一个虚函数来调用,在一个它认为是多线程的应用程序中,当引用计数被改变,会导致线程同步开销。(你可以通过定义一个预处理符号来使多线程支持失效。)在缺点方面,它比一个裸指针大,比一个裸指针慢,而且要使用辅助的动态内存。在许多应用程序中,这些附加的运行时开销并不显著,而对客户错误的减少却是每一个人都看得见的。

Things to Remember

好的接口易于正确使用,而难以错误使用。你应该在你的所有接口中为这个特性努力。使易于正确使用的方法包括在接口和行为兼容性上与内建类型保持一致。预防错误的方法包括创建新的类型,限定类型的操作,约束对象的值,以及消除客户的资源管理职责。tr1::shared_ptr 支持自定义 deleter。这可以防止 cross-DLL 问题,能用于自动解锁互斥体(参见 Item 14)等。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有