Effective C++读书笔记(11)

来源:互联网 发布:cisco端口聚合命令 编辑:程序博客网 时间:2024/05/14 16:01

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

Make interfaces easy to use correctly andhard to use incorrectly

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"

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

struct Day { //Month和Year与之类似
explicit Day(int d) :val(d) {} :

    intval;
};

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

一旦放置了正确的类型,限制其值有时候是通情达理的。例如,月仅有12个合法值,所以 Month 类型应该反映这一点。方法之一是用一个枚举来表现月,但是枚举不具备类型安全性。例如枚举能被作为整数使用。一个安全的解决方案是预先确定合法的 Month 的集合:

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

防止可能的客户错误的另一个方法是限制类型内能够做的事情,常见的限制是加上const。实际上,除非你有很棒的理由,否则就让你的类型行为与内置类型保持一致。客户已经知道像 int 这样的类型如何表现,所以你应该努力使你的类型在合理的前提下有同样的表现。例如,如果 a 和 b 是 int,给 a*b 赋值是非法的。

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

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

 

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

Investment* createInvestment();

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

你可以将createInvestment的返回值存入一个类似auto_ptr 或tr1::shared_ptr 智能指针,从而将使用delete的职责交给智能指针,但仍忘记使用智能指针,不如让factory函数在第一现场即返回一个智能指针:

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

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

·    预防错误的方法包括创建新的类型,限定类型的操作,约束对象的值,以及消除客户的资源管理职责。

 

假设从 createInvestment得到一个Investment*指针的客户期望将这个指针传给一个名为getRidOfInvestment的函数,而不是对它使用delete。tr1::shared_ptr 提供了一个需要两个参数(被管理的指针、当引用计数变为零时要调用的deleter)的构造函数。这启发我们创建一个以getRidOfInvestment 为deleter的null tr1::shared_ptr的方法:

std::tr1::shared_ptr<Investment> pInv(0,getRidOfInvestment);

这不会通过编译。tr1::shared_ptr的构造函数坚决要求它的第一个参数应该是一个指针,而0不是一个指针,它是一个int。当然,它能转型为一个指针,但那在当前情况下并不够好,tr1::shared_ptr坚决要求一个真正的指针。用强制转型解决这个问题,因此createInvestment的实现代码看起来是这样:

std::tr1::shared_ptr<Investment>createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);

    retVal =... ; // 令retVal指向正确对象

    returnretVal;
}

tr1::shared_ptr的一个特别好的特性是它自动逐指针地使用deleter以消除另一种潜在的客户错误——“cross-DLL问题。”这个问题发生在:一个对象在一个动态链接库(dynamicallylinked 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应该被使用”的跟踪。

tr1::shared_ptr是一个消除某些客户错误的简单方法,值得我们核计其使用成本。最通用的 tr1::shared_ptr 实现来自于 Boost,其shared_ptr的大小是原始指针的两倍,以动态分配内存用于簿记用途和deleter专属数据,当调用它的deleter时使用一个virtual函数来调用,并在多线程程序修改引用次数时蒙受线程同步化的额外开销(你可以通过定义一个预处理符号来使多线程支持失效。)。在缺点方面,它比一个原始指针大且慢,而且要使用辅助动态内存。在许多应用程序中,这些附加的运行时开销并不显著,而对客户错误的减少却是每一个人都看得见的。

·    好的接口易于正确使用,而难以错误使用。你应该在你的所有接口中为这个特性努力。

·    tr1::shared_ptr 支持自定义 deleter。这可以防止 cross-DLL 问题,能用于自动解锁互斥体(mutex)等。

原创粉丝点击