Effective C++ 学习笔记(十四)

开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。

假设你为一个用来表现日期的class设计构造函数:

class  Date{

  public:

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

};

但它的客户很容易犯至少两个错误,第一,它们会以错误的次序传递参数:

Date  d(30,3,1995);

(键盘上的2就在3旁边)

很多客户端错误可以应为导入新类型而获得预防。真的,在防范“不值得拥有的代码上”,系统类型是你的主要同盟国。

既然这样,让我们导入简单的外覆类型来区分天数,月份和年份,然后于Date构造函数中使用这些类型:

struct Day{

     explict Day(int d) 

        : val(d){  }

     int val;

};

struct Month{

  explict Month(int m)

  :val(m){ }

    int  val;

};


struct Year{

   explict 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好,

但即使structs也已经足够示范:明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。


一旦正确的类型就定位,限制其值有时候是通情达理的。例如一年只有12个有效月份,所以month应该反应这一事实。

办法之一就是利用enum表现月份,但enums不具备我们希望拥有的类型安全性,例如enums可被拿来当一个ints使用

比较安全的解法是预先定义所有有效的months:

class Month{

    public:

       static Month Jan()  {return Month(1);}  //函数,返回有效月份,这些是函数而非对象

       static Month Feb()  {return Month(2);}

       

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

   private:

       explict Month(int m);                                   //阻止生成新的月份这是月份专属数据

};


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


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


预防客户错误的另一个方法是,限制类型内什么事可做,什么事不能做。常见的限制是加上const.

例如条款3曾经说明为什么“以const修饰operator*”的返回类型"可阻止客户因“用户自定义类型”而犯错:

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

下面是另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式:

“除非有好理由,否则应该尽量令你的types的行为与内置types一致”。



客户已经知道像int这样的type有些什么行为,所以你应该努力让你的types在合样合理的前提下也有相同表现

避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。很少有其他性质比得上“一致性”更能导致“接口容易被正确使用”,

也很少有其他性质比得上“不一致性”更加剧接口的恶化。



任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事,如:

导入一个factory函数,它返回一个指针指向Investment*  createInvestment();  

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

如何将createInvestment的返回值存储于一个智能指针如auto_ptr或tr1::shared_ptr内,因而将delete责任推给智能指针。

但万一客户忘记使用智能指针怎么办?许多时候,较佳的接口的设计原则是先发制人,就令factory函数返回一个智能指针:

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

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


实际上,返回tr1::shared_ptr让接口的设计者得以阻止一大群客户犯下资源泄露的错误,tr1::shared_ptr允许当智能指针被建立起来时指定一个资源释放函数

(所谓删除器,"delete")绑定于智能指针身上(auto_ptr就没有这个能耐)。


假设class设计者期许哪些“从createInvestment取得Investment*指针“的客户将该指针传递给一个名为getRidOfInvestment的函数,而不是直接在它身上动刀(delete)

这样一个接口又开启通往另一个客户错误的大门,该错误是”企图使用错误的资源析构机制“(也就是拿delete替换getRidOfInvestment).

createInvestment的设计者可以针对此问题先发制人:返回一个“将getRidOfInvestment绑定删除器(delete)”的tr1::shared_ptr.


tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用计数变成0时将被调用的“删除器”,像这样:

std::tr1::shared_ptr<Investment>                                           //企图创建一个null shared_ptr

pInv(0,getRidOfInvestment);                                                 //并携带一个自定的删除器。

                                                                                              //此式无法通过编译


这不是有效的C++,tr1::shared_ptr构造函数坚持其第一参数必须是个指针,而0不是指针,是个int。

是的,它可被转换为指针,但在此情况下并不够好,因为tr1::shared_ptr坚持要一个不择不扣的指针。

std::tr1::shared_ptr<Investment>

pInv(static_cast<Investment*>(0),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再对它做

一次赋值操作”为佳。


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-DLLproblem"这个指向stock的tr1::shared_ptr会追踪记录“当Stock的引用次数变成0时该调用那个DLL's delete”


本条款并非特别针对tr1::shared_ptr,而是为了“让接口容易被正确使用,不容易被误用”而设但由于tr1::shared_ptr如此容易消除某些客户错误,值得我们合计其使用成本

最常见的tr1::shared_ptr实现品来自boost,Boost的shared_ptr是原始指针的两倍大,以动态分配内存作为普及用途和“删除器之专属数据”,以virtual形式调用删除器,

并在多线程程序修改引用计数时蒙受线程同步化的额外开销。

总之,它比原始指针大且慢,而且使用辅助动态内存。在许多应用程序中这些额外的执行成本并不显著然而其“降低客户错误的”成效却是每个人都看得到。










  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值