第四章、设计与声明

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

  1. C++包含多种接口,比如:function接口、class接口、template接口。理论上当这些接口被错误的调用了,则接口的实现者也有一部分责任。如果客户使用了接口但未获得他预期的行为,则该接口不应该通过编译。
  2. 促进正确使用接口的方法包括保证接口的一致性,以及与内置类型的行为兼容。
  3. 阻止误用的方法包括建立新类型、限制类型上的操作、束缚对象值、以及消除客户的资源管理责任等。
  4. 对于确定类型的参数而言,限制传入值是合法的,比如我们可以用enum限制传入的ints,但是enum并不具备我们希望拥有的类型安全性。比较安全的方式是提前定义好所有有效的数据。

比如在需要传入一个月份时,可以按照如下方式实现:

class Month
{
public:
    static Month Jan() {return Month(1);};
    static Month Feb() {return Month(2);};
    ...
    static Month Dec() {return Month(12);};
private:
    explicit Month(const int month);
    ...
}
Data(Month::Jan());
  1. 任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户有可能忘记做这件事。
  2. tr1::shared_ptr会自动调用每个指针专属的删除器,可以消除所谓的“cross-DLL problem”错误。该错误发生于“对象在动态链接库中被调用,却在另一个DLL内被delete销毁”。在许多平台上,这类“跨DLL的new/delete成对调用”会导致运行期错误。由于tr1::shared_ptr缺省的删除器是来自于tr1::shared_ptr诞生时所在的那个DLL中,所以使用tr1::shared_ptr时不存在上述问题。
  3. Boost中的shared_ptr为例,该类型的对象大小是原始指针的两倍大,以动态分配内存作为簿记用途和“删除器的专属数据”,以virtual形式调用删除器,并在多线程程序修改引用次数时蒙受线程同步化的额外开销。它比原始指针大且慢,而且使用辅助动态内存,不过其“降低客户错误”的成效较好,我们可根据自身需要选择是否使用它。

条款19、设计class犹如设计type(Treat class design as type design)

  1. 设计class需要考虑如下问题:
    1. 该class的对象应该如何被创建和销毁?

这决定了该class的构造函数、析构函数,以及内存分配和释放函数应该如何实现。

  1. 对象的初始化和对象的赋值应该有什么样的差别?

这决定了该class的构造函数和赋值操作符的行为及差异。

  1. 该class的对象如果被pass-by-value,意味着什么?

copy构造函数定义了pass-by-value该如何实现。

  1. 什么是新类型的“合法值”?

对该类的成员变量而言,通常只有某些数值集合是有效的,这些有效的数据集合决定了该class必须维护的约束条件,也决定了成员函数必须进行的错误检查工作,同时影响函数抛出的异常,以及函数异常明细列。

  1. 该类型是否需要配合某个继承体系?

如果该class需要继承或被继承,则需要考虑继承体系中的virtual和non-virtual对该class的影响。

  1. 新类型是否需要某种转换?

如果需要类型转换,则需要定义隐式或显式转换函数(operator T2或non-explicit-one-argument构造函数)。

  1. 什么样的操作符和函数对此类型而言是合理的?

这决定了你需要的函数里哪些是member函数,哪些不是。

  1. 什么样的标准函数应该被驳回?

这些函数应该被声明为private。

  1. 谁该取用新类型的成员?

这决定了成员的属性,以及哪些class或者function应为friend。

  1. 什么是新类型的“未声明接口”?

它对效率、异常安全性以及资源运用提供何种保证?你提供的这些保证将为你的class实现加上相应的约束条件。

  1. 该类型有多么的一般化?

如果你需要定义一个class家族,那最好定义一个新的template。

  1. 你是否需要这个新类型?

如果只是定义一个derived class以便为既有的class提供机能,那说不定定义non-member函数或者template效果更好。

条款20、宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to pass-by-value)

  1. 缺省情况下C++以pass-by-value的方式传递对象至函数。除非你另外指定,否则函数的参数都是实参的复件,而调用端获取到的返回值也是一个复件。这些复件由对象的copy构造函数产出,这会导致pass-by-value耗费不应耗费的时间。
  2. pass-by-reference-to-const通常比pass-by-value高效,并且可以避免切割问题。
  3. pass-by-reference-to-const中,没有任何新对象被创建,所以没有多余的构造和析构函数被调用。而const则告诉调用者在该函数中不会对传入的对象做改变。
  4. 对于大多数STL容器而言,它们内含的东西只比一个指针多一点,但是复制这种对象需要承担复制那些指针中所指的每一样东西的开销。
  5. 对于内置类型、STL的迭代器和函数对象而言,pass-by-value往往更加适当。

条款21、必须返回对象时,别妄想返回其reference(Don’t try to a reference when you must return an object)

  1. 绝不能返回指向local stack对象的pointer或者reference。
  2. 不应返回pointer或reference指向一个local static对象,而该对象有可能会在多个点被同时调用。

const Rational& operator *(const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = …
return result;
}
当执行语句if ((a * b) == (c * d))时,该表达式永远为true。

条款22、将成员变量声明为private(Declare data members private)

  1. 使用成员函数来访问成员变量可以让你对成员变量的处理更加精确。
  2. 将成员函数隐藏在函数接口背后,可以为“所有可能的实现”提供弹性。例如这可以使成员变量被读或被写时轻松的通知其他对象、可以验证class的约束条件以及函数的前提和事后状态、可以执行同步控制等。
  3. protected并不比public更具封装性。对于protected成员和public成员来说,如果成员变量被改变,都会有不可预知的代码受到影响或破坏,可能有太多代码需要重写、重新测试、重新编写文档,重新编译。
  4. 从封装的角度来说,只有两种访问权限:private(提供封装)和public(不提供封装)。

条款23、宁以non-member、non-friend替换member函数(Prefer non-member、non-friend functions to member functions)

  1. 封装的好处是当你修改模块内的某些实现时,可以尽可能少的影响你的客户。
  2. 如果一个non-member函数和一个member函数提供的机能一样,则导致较大封装性的方法是采用non-member non-friend函数。
  3. 从封装的角度看,friend函数和member函数对封装的影响是相同的。
  4. 在C++中,将为某类服务的函数与这个类的声明位于同一个命名空间内比较好。namespace可以跨越多个文件。
  5. 标准程序库中包含多个头文件,而每个头文件中都声明了std的某些机能。这样的话可以允许客户只对他们所用的那部分系统形成编译相依。
  6. 将所有便利函数放在多个头文件但隶属于同一个命名空间,意味着客户可以轻易扩展这组便利函数。

条款24、若所有参数都需要类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)

  1. 如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是non-member。

条款25、考虑写出一个不抛出异常的swap函数(Consider support for a non-throwing swap)

  1. swap两对象值,就是将两个对象的值彼此赋予对方。缺省情况下,swap动作可由标准库提供的swap算法完成。
  2. 对于一个类型,只要该类型支持copying操作,就可以使用缺省的swap来完成该类型对象的置换操作。
  3. 所有的STL容器都有提供public swap成员函数和std::swap特化版本。
//STL的swap实现类似于下面这种
class Widget
{
    public:
    ...
    void swap(Widget& other)
    {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
    ...
};

namespace std
{
    template<> void swap<Widget>(Widget& a, Widget& b)
    {
        a.swap(b);
    }
}
  1. 对于如下代码,是无法通过编译的,原因是我们企图偏特化一个function template,但C++只允许对class template偏特化。所以当你希望偏特化一个函数模板时,通常我们通过函数重载来实现。
template<typename T>
class Widget{...};

//错误版本:试图偏特化函数模板
namespace std
{
   template<typename T>
   void swap< Widget<T> >(Widget<T>& a, Widget<T>& b)
   {a.swap(b);}
}

//错误版本:试图向std空间添加新的重载函数
namespace std
{
   template<typename T>
   void swap(Widget<T>& a, Widget<T>& b)
   {a.swap(b);}
}

//正确版本
namespace myNamespace
{
    template<typename T>
    class Widget{...};

   template<typename T>
   void swap(Widget<T>& a, Widget<T>& b)
   {
       using std::swap;
       a.swap(b);
   }
}
  1. 一般而言,重载函数模板没有问题,不过std这个命名空间比较特殊。客户可以全特化std内的template,但是无法添加新的template(或classes或functions或其他任何东西)到std里面。
  2. 如果默认的swap对你的class或者class template提供可接受的效率,那么你无需做任何事。如果你需要自己实现swap来提高swap的效率(这种情况基本上意味着你的class或者class template使用了pimpl手法),那么你需要作以下的事情:
    1. 提供一个public swap成员函数。
    2. 在这个class所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
    3. 如果你正编写一个class,则需要为你的类特化swap。
    4. 如果你写的是一个class template,则需要为其添加重载函数。
    5. 如果你调用swap函数,请确认包含一个using声明式,以便让std::swap在函数内被曝光可见,然后直接调用swap。
  3. 成员版的swap不应抛出异常。因为swap的一个最好的应用是帮助class提供强烈的异常安全性保障。
  4. 对于声明式using std::swap,其作用是当编译器没有找到该类专属的swap时,可以调用std的swap函数。不过如果你已经针对该类型将std::swap特化,那么特化版本会被选中。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值