***************************************转载请注明出处:http://blog.youkuaiyun.com/lttree********************************************
四、Designs and Declarations
▪ 断断续续到了第四章了,这一章中我们将对 良好C++接口 的 设计和声明 发起攻势。
▪ 我们以 或许最重要、适合任何接口设计的 一个准则作为开端——"让接口容易被正确使用,不容易被误用"
▪ 通过这个准则设立一个舞台,让其他更专精的准则对付一大范围的题目,包括: 正确性、高效性、封装性、维护性、延展性,以及 协议的一致性。
Rule 18:Make interfaces easy to use correctly and hard to use incorrectly.
规则 18:让接口容易被正确使用,不易被误用
1.现象
C++ 一直在 接口之海 漂浮。function接口、class接口、template接口……每一种接口都是客户与你的代码互动的手段。
理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码不该通过编译:如果代码通过了编译,它的作为就应该是客户所想要的。
2.开发" 容易被正确使用,不易被误用 "的接口
>首先,必须考虑客户可能做出什么样的错误。
// 先看一个表现日期的类的构造函数 class Date { public: Date(int month,int day,int year); ... };
上面代码,乍看一下,是没有问题的,可是 暗藏杀机 啊!
① 他们也许会以错误的次序传递参数:Date d(30,3,1995);
② 他们可能传递一个无效的月份或天数:Date d(2,30,1995);
>防范“ 不值得拥有的代码 ” 上, 类型系统(type system )是你的主要同盟国。
1.我们可以导入 外覆类型(wrapper types )来区别 天数、月份、年份。
<span style="font-family:Comic Sans MS;">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); ... };</span>
PS: C++提供了 关键字 explicit,可以阻止不应该允许的经过 转换构造函数 进行的隐式转换的发生。声明为explicit的 构造函数 不能在隐式转换中使用。
2.一旦正确的类型就定位,限制其值有时候是通情达理的,比如,可以用enum来制定月份,但enum不具备我们希望拥有的类型安全性,这有比较安全的解法,就是预先定义所有有效的Months
<span style="font-family:Comic Sans MS;">class Month { public: static Month Jan() { return Month(1); } static Month Feb() { return Month(2); } ... static Month Dec() { return Month(12); } ... private: explicit Month(int m); ... }; Date d(Month::Mar(),Day(30),Year(1995) );</span>
>预防客户错误的另一个办法,限制类型内什么事可以做,什么事不能做
—1.常见的限制 只是 加上 const。
—2.除非有好理由,否则应该尽量令你的types 的行为 与内置 types 一致。
很少有其他性质比得上“一致性”更能导致“接口容易被正确使用”,也很少有其他性质比得上“不一致性”更加剧接口的恶化。
这里就有一点小比较
▪ C++ 的STL容器的接口十分一致(虽然并不是完美的一致),比如每个STL容器都有一个名为 size 的成员函数,作用是告诉用户目前容器内有多少对象。
▪ JAVA ,它允许对数组使用 length property,对Strings 使用 length method, 对 Lists 使用 size method
▪ .NET 对 Arrays 有个 property名为 Length,ArrayLists 有个 property 名为 Count
—3.任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那些事。
比如条款13导入的工厂函数(factory function),它返回一个指针指向 Investment 继承体系内的一个动态分配对象:
Investment* createInvestment();
为避免资源的泄露,createInvestment 返回的指针必须删除,但那至少开启了两个客户错误机会:没有删除指针,或删除同一个指针超过一次。
std::tr1::shared_ptr<Investment> createInvestment();
实际上,返回 tr1::shared_ptr 让接口设计者得以阻止一大群客户犯下的 资源泄漏 错误。
—4.假设 class 设计者期许那些“ 从 createInvestment 取得 Investment* 指针”的客户将 该指针传递给一个名为 getRidOfInvestment的函数,而不是直接在它身上使用delete(就是用 delete 替换 getRidOfInvestment )。
它可以返回一个“将getRidOfInvestment 绑定为删除器(deleter )”的 tr1::shared_ptr
tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时 将被调用的“删除器”。
但是有一点,tr1::shared_ptr 构造函数坚持其第一参数必须是指针,而0不是指针,是个int。虽然它可以被转换成指针,但是这不是特别好,所以用 转型(cast)来解决这个问题:
<span style="font-family:Comic Sans MS;">std::tr1::shared_ptr<Investment> pInv( static_cast<Investment*>(0),getRidOfInvestment);</span>
因此,如果要实现createInvestment使它返回一个 tr1::shared_ptr 并夹带 getRidOfInvestment 函数作为删除器,代码看起来就是这样的:
<span style="font-family:Comic Sans MS;">std::tr1::shared_ptr<Investment> createInvestment() { std::tr1::shared_ptr<Investment> retVal(static_cast<Investment* >(0),getRidOfInvestment); retVal = ... ; // 令 retVal 指向正确对象 return retVal; }</span>
如果被 pInv 管理的原始指针(raw pointer ) 可以在建立 pInv之前 先确定下来,那么 "将原始指针传给pInv构造函数" 会比 "先将pInv初始化为null 再对它做一次赋值操作 " 为佳。(具体原因,将会在条款26给出)
—5 tr1::shared_ptr 有一个特别好的性质:它会自动使用它的" 每个指针专属的删除器 ",因而消除另一个潜在的客户错误:所谓的“ cross-DLL problem ”(就是一个DLL中new,但是在另一个DLL中 delete )。
4.请记住关于这个条款,要明确一下,本条款并非特别针对 tr1::shared_ptr ,而是为了“ 让接口更容易被正确使用,不容易被误用 ”。为什么本条款对 tr1::shared_ptr 讲了这么这么多? 因为它 是如此的容易消除错误,值得我们使用。 最常见的 tr1::shared_ptr 实现来自Boost。Boost的shared_ptr 比 原始指针 大 且 慢,而且使用辅助动态内存。在许多应用程序中这些额外的执行成本并不显著,然而其“ 降低客户错误 ”的成效却是每个人都看得到。
★ 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质
★ “促进正确使用” 的办法包括接口的一致性,以及与内置类型的行为兼容
★ “阻止误用” 的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
★ tr1::shared_ptr 支持定制型删除器( custom deleter )。这可防范DLL 问题,可被用来自动解除 互斥锁(mutexes)等等
***************************************转载请注明出处:http://blog.youkuaiyun.com/lttree********************************************