item18 让接口容易被正确使用,不易被误用

一个例子

  1. class Date {
  2. public:
  3. Date(int month, int day, int year);
  4. ...
  5. };
  • 输入无效的数据(超过范围)
  • 输入顺序错误 :::tips 解决方式:

  • 预定义所有有效值

  • 限制类型内什么可以做什么不可以
  • 除非有好的理由,否则应该尽量让自己定义的types行为与内置的types一致
  • 消除客户对于管理资源的责任 :::

    另一个例子

    比如之前的factory函数的例子,当调用factory函数是,返回一个指针
    1. std::tr1::shared_ptr<Investment> createInvestment();
    这种方式强迫客户将返回值存储在shared_ptr中,消除了忘记删除底部对象的可能;不过这又有新的错误可能 :::info 例如,我们希望给智能指针的删除方法是自定义的getRidOfInvestment,但是可能客户会忘记; ::: 我们的结局思路是,在createInvestment中先构建一个绑定好的空shared_ptr,然后重新赋值 :::info tr1::shared_ptr提供某个构造函数接受两个实参
  1. 需要管理的指针
  2. 删除器 :::
    1. std::tr1::shared_ptr<Investment> createInvestment(){
    2. std::tr1::shared_ptr<Investment> retVal(static_cast<Investment* >(0),
    3. getRidOfInvestment);
    4. retVal=...真正值
    5. return retVal;
    6. }
    :::tips shared_ptr的另一个好性质是可以自动使用每个指针专属的删除器;
    这可以消除cross-DLL problem:
  • 如果对象在一个DLL中被new创建,在另一个DLL内被delete,跨DLL的new/delete可能导致崩溃
  • 但是shared_ptr会缺省的删除器是来自shared_ptr诞生的那个DLL。 :::

    item19 设计class犹如设计type

    :::info 应当带着“语言设计者当初设计语言内置类型时”一样的谨慎来研讨 class 的设计。 ::: 几乎每一个 class 都要求你面对以下提问:

  • 新 type 的对象应该如何被创建和销毁?

这会影响 class 的构造函数和析构函数,以及内存分配函数和释放函数(new,new[],delete,delete[])

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

这会影响你的构造函数和赋值操作符的行为

  • 新 type 的对象如果被 passed by value(以值传递),意味着什么?

这会影响 copy 构造函数

  • 什么是新 type 的“合法值”?

所有成员函数需要进行错误检查工作,进行必要的约束,处理异常

  • 你的新 type 需要配合某个继承图系吗?

如果继承自既有 class,你就会收到这些 classes 设计的束缚,特别是收到它们的函数是 virtual 或 non-virtual 的影响。如果你允许其他 classes 继承你的 class,会影响你所申明的函数,尤其是析构函数是否为 virtual

  • 什么样的操作符和函数对此新 type 而言是合理的?

这会影响你为你的 class 申明哪些函数

  • 你的新type需要什么样的转换?

如果允许隐式转换,那么要考虑写转换函数以及是否允许explicit的构造函数

  • 什么样的标准函数应该驳回?

如果不需要编译器默认生成的版本,需要自行申明为 private 拒绝

  • 谁该取用新 type 的成员?

这会影响你 class 所有成员的可访问级别,以及哪些是 friends

  • 什么是新 type 的“未申明接口”?

它对效率、异常安全性以及资源运用提供何种保证?

  • 你的新 type 有多么一般化?

或许你并非定义一个新 type,而是定义一整个 types 家族,此时应该定义一个新的 class template 而非 class

  • 你真的需要一个新 type 么?

如果只是添加一些功能,也许淡出添加些 non-member 函数或 templates 就能达到目标

item20 宁以pass-by-reference替换pass-by-value

:::tips

  • 尽量以pass-by-reference替换pass-by-value。前者通常高效,并且可以避免切割问题
  • 以上规则并不适用于内置类型、STL的迭代器以及函数对象,对他们而言pass-by-value更适当。 ::: 缺省情况下C++以by value方式传递对象给函数,此外函数返回值也是一个复制值。这对于函数而言有时会造成比较大的开销:一次copy构造函数的调用,一次析构函数调用。 :::info 对象切割是什么问题?
    当一个派生类对象被用pass-by-value的方式传递给一个基类对象时,基类对象的拷贝构造函数只能有权限复制基类的部分。但是如果使用pass-by-reference的话,会起到多态的效果。 ::: :::success 实际上在C++编译器的底层,reference往往是以指针实现出来的,pass-by-reference通常意味着传递的是指针。 :::

    item21 必须返回对象时,别妄想返回其reference

    :::info 绝不要返回pointerreference指向一个 local stack 对象,或返回 reference 指向一个 head-allocated 对象,或返回 pointerreference 指向一个 local static 对象设置有可能需要多个这样的对象。 ::: 如果从节约资源的角度考虑,那么函数返回一个reference肯定比返回一个值要节约一次拷贝构造函数。

  • 函数创建新对象:在stack或者heap空间上创建一个对象

    • local变量——在stack上创建,如果返回其引用,那么函数结束生命周期时,该局部变量会销毁,引用非法。
    • new一个对象——在heap上创建,谁来销毁是个问题
  • 在类内部定义一个static对象;

    • 多线程安全问题
    • 比较相等时会出错。

      item22 将成员变量声明为private

      如果public接口内所有东西都是函数,那么用户不需要在打算访问class成员时迷惑地试图记住是否该使用小括号;此外,使用函数可以对成员变量更精准的控制,如:可以实现只读、可写等等约束;
      1. class accessLevels{
      2. public:
      3. //...
      4. int getReadOnly() const{return readOnly;}
      5. void setWriteOnly(int val){ writeOnly=val;}
      6. private:
      7. int readOnly;
      8. int writeOnly;
      9. };
      :::info 绝大多数变量是不需要从外部访问的,因此可以将所有变量变成private,只需要给必须的变量留接口。 ::: 封装的好处: :::tips
  • 可以确保class的约束始终被维护

  • 可以避免与客户的代码过度耦合
  • 成员变量的封装性与“当其内容改变时可能造成的代码破坏量”成反比;

    • 例如:如果一个public成员变量,我们取消了它,那么所有涉及该变量的代码都需要重写
    • 如果一个protected成员变量,我们取消了它,那么所有使用它的派生类都会被破坏。 ::: :::danger 实际上只有两种封装:private和其他 :::
  • 切记将成员变量声明为private。这赋予客户访问数据的一致性、可细微划分的范围控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。

  • protected不必public更具有封装性。

    item23 宁以non-member、non-friend替换member函数

    一个例子:
    例如有一个class表示网页浏览器;
    1. class WebBrowser{
    2. public:
    3. void clearCache();
    4. void clearHistory();
    5. void removeCookies();
    6. };
    如果有一个操作想要实现清除以上所有信息,有两种实现:member function or non-member function
    1. class WebBrowser{
    2. public:
    3. void clearEverything();
    4. }
    5. //--------------------------------------------------
    6. void clearBrowser(WebBrowser& wb){
    7. wb.clearCache();
    8. wb.clearHistory();
    9. wb.removeCookies();
    10. }
    :::tips 问题来了,哪个版本更好? ::: 先说答案,从控制封装的角度看用non-member function更好!

    如何衡量成员的封装性?

    :::info 简单的说,我们计算能够访问该成员变量的函数数量,作为一种粗糙的测量。愈多函数可访问它,数据的封装性就越低。 ::: 因此对于一个函数而言,non-member version如果和member version机能相同,那么从封装的角度考虑,non-member version可能会更合适,当然不止需要从封装角度考虑,还需要从别的角度(如隐式类型转换)

成为class 的non-member并不意味着不可以是另一个class的member;比如可以令函数是某个工具类的成员。 :::tips 在C++中,比较自然的方法是定义一个non-member function,并且位于和类同一个命名空间内。 :::

  1. namespace WebBrowserSutff{
  2. class WebBrowser{};
  3. void clearBrowser(WebBrowser& wb);
  4. //namespace可以跨越多个源码文件。
  5. }

如何分离便利函数

大多数情况下,用户可能只会对某个便利函数感兴趣,如果全部编译需要时间较多,如何分离编译相依的关系? :::tips 分离声明所在的头文件 :::

  1. //webbrowser.h
  2. namespace WebBrowserSutff{
  3. class WebBrowser{};
  4. }
  5. //webbrowserbookmarks.h
  6. namespace WebBrowserSutff{
  7. void clearbookmark(WebBrowser& wb);
  8. //与书签相关的便利函数
  9. }
  10. //webbrowsercookies.h
  11. namespace WebBrowserSutff{
  12. void clearcookie(WebBrowser& wb);
  13. //与cookie相关的便利函数
  14. }

:::info C++STL的组织方式也是这样的!将多个便利函数放在不同头文件但是隶属于同一个命名空间。 :::

item24 若所需参数皆需要类型转换,请采用non-member函数

:::info 如果需要为某个函数的所有参数(包括this指针指向的隐含参数)进行类型转换,此函数必须是non-member ::: 考虑一个有理数类:

  1. class Rational{
  2. public:
  3. Rational(int numerator=0,int denominator=1);
  4. int numerator() const;
  5. int denominator() const;
  6. private:
  7. ..
  8. };

例如我们要实现乘法operator*采用member 还是non-member?

member function带来的问题

  1. const Rational Rational::(const Rational& rhs) const;

这种情况下定义的乘法可以支持两个对象相乘,但是对于混合运算可能出问题。

  1. Rational oneHalf(1,2);
  2. Rational result;
  3. result = oneHalf*2;
  4. //result = oneHalf.operator*(2);
  5. //执行了隐式的类型转换
  6. //如果Rational的构造是explicit的,那么也不可以!
  7. result = 2*oneHalf;//wrong!
  8. //result = 2.operator*(oneHalf);

:::success 解决方案:让operator*成为一个non-member function

  • 只有当参数被列于参数列内,此参数才是隐式类型转换的合格参与者
  • 如果用成员函数来表示的话,必然会隐含this指针,不在参数列表内 :::
    1. const Rational operator*(const Rational& lhs,const Rational& rhs);

    item25 考虑写出一个不抛异常的swap函数

    swap置换两个对象的值,在缺省时会使用标准库提供的swap算法
    1. namespace std{
    2. template<typename T>
    3. void swap(T& a,T& b){
    4. T temp(a);
    5. a=b;
    6. b=temp;
    7. }
    8. }
    :::tips 缺陷是会复制三次!对于某些类而言没有必要。——哪些类?pimpl:(pointer to implementation) ::: ```cpp class WidgetImpl { public: WidgetImpl(); ~WidgetImpl();

private: int a, b, c; vector v; }; //————————————————————- class Widget { public: Widget(); ~Widget(); Widget& operator=(const Widget& rhs) { pImpl = (rhs.pImpl); return *this; }

private: WidgetImpl* pImpl; };

  1. :::info
  2. pimpl:用指针指向一个对象,内含真正的数据
  3. - 如果想要交换两个Widget的值,实际上只需要交换pImpl指针;
  4. - 但是缺省的`swap`算法不仅复制3`Widget`,还会复制三个`WidgetImpl`对象(why?**看**`**operator=**`**的操作可以看出来**!)
  5. :::
  6. <a name="sYdsi"></a>
  7. ## 修改方案1
  8. 直接在namespace std中定义特化的swap可以解决,但是无法访问私有对象;
  9. :::tips
  10. 通常我们不允许改变std命名空间中的任何东西,但是可以被允许为标准template制造特化版本。
  11. :::
  12. ```cpp
  13. class Widget{
  14. public:
  15. void swap(Widget& other) {
  16. using std::swap;
  17. swap(pImpl, other.pImpl);
  18. }
  19. };
  20. namespace std {
  21. template<>
  22. void swap<Widget>(Widget& a, Widget& b) {
  23. a.swap(b);
  24. }
  25. }

:::info 解决方案如上:

  • 在std域中定义全特化版本的swap,其实现是通过调用类内的swap
  • 类内的swap采用std中普通的swap来交换指针
  • 这样做维持了拷贝构造函数的深拷贝,并且节约了交换时的开销; :::

    对于template类的特化

    :::info C++只允许对class templates偏特化,在function templates上偏特化不被允许。 :::

    1. namespace std {
    2. template<typename T>
    3. void swap<Widget<T>>(Widget<T>& a, Widget<T>t& b){
    4. a.swap(b);
    5. }
    6. }

    因此考虑采用重载; :::info 但是std命名空间里不允许添加新的templates! :::

    1. namespace std {
    2. template<typename T>
    3. //重载的swap,swap后没有<>
    4. void swap(Widget<T>& a, Widget<T>t& b){
    5. a.swap(b);
    6. }
    7. }
    8. //std空间不允许修改!达咩!

    解决方案:

  • 定义一个命名空间;

  • 采用non-member版本的swap,通过调用member版本swap实现
  • 将swap和class都放入此空间;

    1. namespace WidgetStuff {
    2. template<typename T>
    3. class Widget{
    4. public:
    5. void swap(const Widget& rhs){
    6. using std::swap;
    7. swap(pImpl,other.pImpl);
    8. }
    9. };
    10. template<typename T>
    11. void swap(Widget<T>& a, Widget<T>& b) {
    12. a.swap(b);
    13. }
    14. }

    C++编译器查找规则

    考虑如下代码

    1. template<typename T>
    2. void doSomething(T& obj1,T& obj2){
    3. using std::swap;
    4. swap(obj1,obj2);
    5. }

    :::info

  • 名称查找法则(name lookup rules):寻找global作用域或者T所在的命名空间内任何T专属的swap

  • 实参取决之查找规则:在T的作用域内找的时候安装此规则
  • 鉴于使用了std::swap,可以找std中的swap。 :::

    summary

  • std::swap对某类型效率不高时,提供一个swap成员函数,并确定此函数不抛出异常;

  • 如果提供了一个member swap,也应该给出一个non-member swap来调用他。对于class而言,需要特化std::swap
  • 调用swap时,应该针对std::swap使用using,然后调用swap并且不带任何命名空间资格修饰
  • 可以为用户定义的类型进行std全特化,但是不要在std中加入新的东西。