1. 什么样的接口才是好的接口

C++中充斥着接口:函数接口,类接口,模板接口。每个接口都是客户同你的代码进行交互的一种方法。假设你正在面对的是一些“讲道理”的人员,这些客户尝试把工作做好,他们希望能够正确使用你的接口。在这种情况下,如果接口被误用,你的接口应该至少负一部分的责任。理想情况下,如果使用一个接口没有做到客户希望做到的,代码应该不能通过编译;如果代码通过了编译,那么它就能做到客户想要的

2. 编写好的接口的方法列举

2.1 使接口不容易被误用——通过引入新的类型

开发出容易被正确使用不容易被误用的接口需要你考虑客户可能出现的所有类型的错误。举个例子,假设你正在为一个表示日期的类设计一个构造函数:

1 class Date {
2
3 public:
4
5 Date(int month, int day, int year);
6
7 ...
8
9 };

乍一看,这个接口可能看上去去合理的,但是客户很容易犯至少两种错误。

第一,他们可能搞错参数的传递顺序

1 Date d(30, 3, 1995); // Oops! Should be “3, 30” , not “30, 3”

第二,他们可能传递一个无效的月份或者天数(day number):

1 Date d(3, 40, 1995); // Oops! Should be “3, 30” , not “3, 40”

(最后一个例子看上去很病态,但是不要忘了在键盘上,数字4和3是挨着的,将3错打成4这样的错误不是不常见。)

通过引入新的类型,许多客户错误就能被避免。确实,类型系统(type system)是你阻止不合要求的代码编译通过的主要盟友。在这种情况下,我们可以引入简单的包装类型来区分天,月和年,然后在Date构造函数中使用这些类型:

 1 struct Day{
 2 explicit Day(int d): val(d) {}
 3 int val;
 4 };
 5 struct Month {
 6 explicit Month(int m): val(m) {}
 7  int val;
 8  };
 9 struct Year {
10 explicit Year(int y): val(y){}
11  int val;
12  };
13 class Date {
14 public:
15 Date(const Month& m, const Day& d, const Year& y);
16 ...
17 };
18 Date d(30, 3, 1995); // error! wrong types
19 Date d(Day(30), Month(3), Year(1995)); // error! wrong types
20 Date d(Month(3), Day(30), Year(1995)); // okay, types are correct

将Day,Month和Year数据封装在羽翼丰满的类中比上面简单的使用struct要更好(Item 22),但是使用struct就足以证明,明智的引入新类型可以很好的阻止接口被误用的问题。

一旦正确的类型准备好了,就能够合理的约束这些类型的值。举个例子,只有12个月份应该能够通过Month类型反映出来。一种方法是使用一个枚举类型来表示月份,但是枚举不是我们喜欢的类型安全的类型。例如,枚举可以像int一样使用(Item 2)。一个更加安全的解决方案是预先将所有有效的月份都定义出来。

 1 class Month {
 2
 3 public:
 4
 5 static Month Jan() { return Month(1); } // functions returning all valid
 6
 7 static Month Feb() { return Month(2); } // Month values; see below for
 8
 9 ... // why these are functions, not
10
11 static Month Dec() { return Month(12); } // objects
12
13 ... // other member functions
14
15 private:
16
17 explicit Month(int m); // prevent creation of new
18
19 // Month values
20
21 ... // month-specific data
22
23 };
24
25 Date d(Month::Mar(), Day(30), Year(1995));

如果使用函数代替对象来表示指定月份值会让你觉的奇怪的话,可能是因为你忘记了非本地static对象的初始化是有问题的(见 Item 4)。

2.2 使接口不容易被误用——对类型的操作进行限定

另外一种防止类似错误的方法是对类型能够做什么进行限制。进行限制的一般方法是添加const。举个例子,Item 3解释了对于用户自定义的类型,把operator*的返回类型加上const能够防止下面错误的发生:

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

2.3 使接口容易被正确使用——提供行为一致的接口

事实上,这只是“使类型容易正确使用不容易被误用”的另外一个指导方针的表现形式:除非有更好的理由,让你的自定义类型同内建类型的行为表现一致。客户已经知道像int一样的内建类型的行为是什么样子的,所以在任何合理的时候你应该努力使你的类型表现与其一致。举个例子,如果a和b是int类型,那么赋值给a*b是不合法的,所以除非有一个好的理由偏离这种行为,你应该使你的类型同样不合法。每当你不确定自定义类型的行为时,按照int来做就可以了。

防止自定义类型同内建类型无端不兼容的真正原因是提供行为一致的接口。没有特征比“一致性”更能使接口容易被使用了,也没有特征比“不一致性”更加导致接口容易被误用了。STL容器的接口大体上(虽然不是完全一致)是一致的,这使得它们使用起来相当容易。举个例子,每个STL容易有一个size成员函数,用来指出容器中的对象数量。与Java相比,arrays使用length属性(property)来表示对象数量,而String使用length方法(method)来表示,List使用size方法来表示;对于.NET来说,Array有一个Length属性,而ArrayList有一个Count属性。一些开发人员认为集成开发环境(IDE)使这种不一致性不再重要,但他们错了。不一致性会将精神摩擦强加到开发人员的工作中,没有任何IDE能够将其擦除。

2.4 使接口不容易被误用——使用shared_ptr消除客户管理资源的责任

2.4.1 让函数返回一个智能指针

一个要让客户记住做某事的接口比较容易被用错,因为客户有可能会忘记做。举个例子,Item 13中引入一个工厂函数,在一个Investment继承体系中返回指向动态分配内存的指针:

1 Investment* createInvestment(); // from Item 13; parameters omitted
2
3 // for simplicity

为了防止资源泄漏,createInvesment返回的指针最后必须被delete,但是这为至少两类客户错误的出现创造了机会:delete指针失败,多次delete同一个指针。

Item 13展示了客户如何将createInvestment的返回值存入像auto_ptr或者tr1::shared_ptr一样的智能指针中,这样就将delete的责任交给智能指针。但是如果客户忘记使用智能指针该怎么办?在许多情况下,更好的接口是要先发制人,让函数首先返回一个智能指针

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

这就强制客户将返回值保存在tr1::shared_ptr中,从而完全消除了忘记delete不再被使用的底层Investment对象的可能性。

2.4.2 返回绑定删除器的智能指针

事实上,对于一个接口设计者来说,返回tr1::shared_ptr能够避免许多其他的有关资源释放的客户错误,因为Item 14中解释道,在创建智能指针时,tr1::shared_ptr允许将一个资源释放函数——释放器(deleter)——绑定到智能指针上。

假设客户从createInvestment得到一个Investment*指针,我们通过将这个指针传递给一个叫做getRidOfInvestment的函数来释放资源而不是直接使用delete。这样的接口开启了另外一类客户错误的大门:客户可能会使用错误的资源析构机制(用delete而不是用提供的getRidOfInvestment接口)。createInvestment的实现者可以先发制人,返回一个tr1::shared_ptr,并将getRidOfInvestment绑定为删除器

Tr1::shared_ptr提供了一个有两个参数的构造函数:需要被管理的指针和当引用计数为0时需要被调用的删除器。这就提供了一个创建用getRidOfInvestment作为删除器的空tr1::shared_ptr的方法:

1 std::tr1::shared_ptr<Investment> // attempt to create a null
2
3 pInv(0, getRidOfInvestment); // shared_ptr with a custom deleter;
4
5 // this won’t compile

上面不是有效的c++,tr1::shared_ptr构造函数的第一个参数必须为指针,但是0不是指针。虽然它可以转换成指针,但是在这个例子中不够好;tr1::shared_ptr坚持使用真实的指针。一个cast就能解决问题:

1 std::tr1::shared_ptr<Investment> // create a null shared_ptr with
2
3 pInv( static_cast<Investment*>(0), // getRidOfInvestment as its
4
5 getRidOfInvestment); // deleter; see Item 27 for info on
6
7 // static_cast

这意味着实现一个createInvestment的代码如下(返回值为绑定了getRidOfInvestment作为删除器的tr1::shared_ptr):

 1 std::tr1::shared_ptr<Investment> createInvestment()
 2
 3 {
 4
 5 std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
 6
 7 getRidOfInvestment);
 8
 9 ... // make retVal point to the
10
11 // correct object
12
13 return retVal;
14
15 }

当然,如果在创建一个retVal之前就能够决定一个原生指针是不是由reVal来管理,将原生指针直接传递给retVal的构造函数比先将retVal初始化为null然后做一个赋值操作要好。为什么请看 Item 26。

2.5 使用智能指针消除交叉-DLL错误

Tr1::shared_ptr的一个特别好的性质是它可以用它的删除器来消除另外一个客户错误——交叉(cross)-DLL错误。当一个对象在一个DLL中使用new被创建,但是在另外一个DLL中被delete时这个问题就会出现。在许多平台中,这样的交叉-DLL new/delete对会导致运行时错误。使用tr1::shared_ptr可以避免这种错误,因为它使用的默认的删除器来自创建tr1::shared_ptr的DLL。这就意味着,例如,如果Stock是一个继承自Investment的类,createInvestment实现如下:

1 std::tr1::shared_ptr<Investment> createInvestment()
2
3 {
4
5 return std::tr1::shared_ptr<Investment>(new Stock);
6
7 }

返回的tr1::shared_ptr可以在DLL之间被传递而不用考虑cross-DLL问题。在Stock的引用计数为0的时候,指向Stock的tr1::shared_ptr指针会追踪哪个DLL的删除器被用来释放资源。

3.使用智能指针的代价

这个Item不是关于tr1::shared_ptr的——它是关于“使接口容易被正确使用不容易被误用”这个议题的——但是使用tr1::shared_ptr是一个如此容易的消除客户错误的方法,所以值得将使用它的代价做一个概述。Tr1::shared_ptr的最一般的实现来自Boost(Item 55)。Boost中的shared_ptr占用内存是原生指针的两倍,为bookkeeping(引用计数)和deleter-specific(专属删除器) 数据分配动态内存,调用删除器的时候使用虚函数,当在一个应用中修改引用计数时,如果它认为自己是多线程的,会引发线程同步开销。(你可以通过定义一个预处理符号来disable多线程支持)一句话,它比原生指针占用内存多,比原生指针慢,并且使用了辅助的动态内存。但是在许多应用中,这些额外的运行时开销是不明显的,但是客户错误的消除对每个人来说都是显而易见的。

4.总结

  • 好的接口容易被正确使用不容易被误用,你应该使所有的接口满足这两个特征。
  • 接口被正确使用的方法包括接口的一致性和同内建类型的行为兼容。
  • 接口不容易被误用的方法包括,创建新的类型,对类型上的操作进行限制,约束对象值,去除客户管理资源的责任。
  • Tr1::shared_ptr支持个性化删除器。这避免了交叉-DLL问题,可以被用来自动unlock互斥器(Item 14)等等。

转载于:https://www.cnblogs.com/harlanc/p/6431766.html

读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用相关推荐

  1. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

  2. 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

    1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), 1 class Widget { 2 public: 3 Widget(); 4 vi ...

  3. 读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的

    1. 自定义new和delete的三个常见原因 我们先回顾一下基本原理.为什么人们一开始就想去替换编译器提供的operator new和operator delete版本?有三个最常见的原因: 为了检 ...

  4. 读书笔记 effective c++ Item 49 理解new-handler的行为

    1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...

  5. 读书笔记 effective c++ Item 47 使用traits class表示类型信息

    STL主要由为容器,迭代器和算法创建的模板组成,但是也有一些功能模板.其中之一叫做advance.Advance将一个指定的迭代器移动指定的距离: 1 template<typename Ite ...

  6. 读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

    正文 最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热 ...

  7. 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

    1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...

  8. 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式

    1. 一个错误释放内存的例子 下面的场景会有什么错? 1 std::string *stringArray = new std::string[100]; 2 3 ... 4 5 delete str ...

  9. [读书笔记]Effective C++ - Scott Meyers

    [读书笔记]Effective C++ - Scott Meyers 条款01:视C++为一个语言联邦 C++四个次语言: 1. C Part-of-C++,没有模板.异常.重载. 2. Object ...

最新文章

  1. WPF 中动态创建和删除控件
  2. Liunx UID and GID
  3. 如何用ARP欺骗来嗅探主机流量
  4. oracle表名最大长度6,Oracle中表名的最大长度是多less?
  5. IL系列文章之二:Make Best Use of Our Tools
  6. vim简单命令教程-firstblood
  7. sql2000 版本号
  8. 利用vector进行图的存储
  9. 最长递增字串的三种做法
  10. [转载]在ASP.NET中使用Microsoft Word文档
  11. 再次了解深浅拷贝问题
  12. tensorflow之relu
  13. java和C#的区别汇总
  14. Java关于周跨年的周数计算,编写一个JAVA类,用于计算两个日期之间的周数。
  15. arma模型 java_ARMA模型与ARIMA模型java实现例程
  16. 手机上php文件用什么打开方式,php是什么文件格式 php文件打开方法【图文】
  17. 数字化制造的世界最高水平,看灯塔工厂如何推进数字化
  18. 手机查看企业qq邮件服务器,QQ企业邮箱怎么用?手机QQ邮箱收发邮件的方法
  19. python和c 情侣网名_qq情侣网名个性网
  20. 【博主推荐】html好看的爱心告白源码

热门文章

  1. nodejs 服务器怎么导入qs_nodejs基本原理总结
  2. python流程控制语法_005 Python语法之流程控制
  3. 论文阅读——《Exposure Control using Bayesian Optimization based on Entropy Weighted Image Gradient》
  4. Haar-like特征
  5. 【阿里云课程】深度学习模型设计:卷积核的设计与优化
  6. 【GAN优化】GAN优化专栏栏主小米粥自述,脚踏实地,莫问前程
  7. 加入微信洗稿投诉小组1个月后,有三说说为什么坚持只发原创
  8. [caffe解读] caffe从数学公式到代码实现3-shape相关类
  9. 全球及中国商用壁挂式浴镜行业投资决策与需求前景预测报告2022版
  10. JuJu团队12月28号工作汇报