Effective C++条款18:让接口容易被正确使用,不容易被误用
创始人
2024-03-03 12:06:43
0

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

  • 条款18:让接口容易被正确使用,不容易被误用
    • 1、编写好的接口的方法
      • 1.1 引入新的类型
      • 1.2 对类型的操作进行限定
      • 1.3 提供行为一致的接口
      • 1.4 使用智能指针消除客户管理资源的责任
        • 1.4.1 让函数返回一个智能指针
        • 1.4.2 返回绑定删除器的智能指针
      • 1.5 使用智能指针消除交叉-DLL错误
    • 2、牢记
  • 总结


《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


条款18:让接口容易被正确使用,不容易被误用

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

1、编写好的接口的方法

1.1 引入新的类型

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

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

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

  • 第一,他们可能搞错参数的传递顺序:
Date d(30, 3, 1995);
  • 第二,他们可能传递一个无效的月份或者天数:
Date d(2, 30, 1995); 

(上一个例子看上去很蠢,但是不要忘了在键盘上,数字2和3是挨着的,将2错打成3这样的错误并不罕见。)

  许多客户端错误可以因为通过引入新的类型获得预防,的确,类型系统(type system)是你阻止不合要求的代码编译通过的主要盟友。在这种情况下,我们可以引入简单的外覆类型来区分天,月和年,然后在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); // error! 错误类型
Date d(Day(30), Month(3), Year(1995)); // error!  错误类型
Date d(Month(3), Day(30), Year(1995)); //  OK,正确类型

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

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

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));

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

1.2 对类型的操作进行限定

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

if (a * b = c) ... //原意是要做一次比较动作

1.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能够将其擦除。

1.4 使用智能指针消除客户管理资源的责任

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

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

Investment* createInvestment(); 

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

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

std::tr1::shared_ptr createInvestment();

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

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

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

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

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

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

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

std::tr1::shared_ptr // 创建一个 null shared_ptr
pInv( static_cast(0), // 并以getRidOfInvestment作为删除器
getRidOfInvestment); 

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

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

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

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

  tr1::shared_ptr的一个特别好的性质是它会自动使用它的“每个指针专属的删除器”,因而消除另外一个客户错误——交叉(cross)-DLL错误。

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

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

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

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

2、牢记

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

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

  • “阻止误用”的方法包括,建立新的类型,限制类型上的操作,约束对象值,去除客户管理资源的责任。

  • tr1::shared_ptr支持个性化删除器。这避免了交叉-DLL问题,可以被用来自动unlock互斥器条款14等等。

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...