0%

Effective C++之设计与声明

4章:设计与声明

条款18:让接口容易被正确使用,不容易被误用
  • 如果客户企图使用某个接口却没有获得所预期的行为,这个代码不应该通过编译;如果代码通过了编译,则它的行为就应该是客户想要的
  • 欲达“让接口容易被正确使用,不容易被误用”的目的,必须考虑客户调用接口时可能做出什么样的错误
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型兼容
  • “阻止误用”的办法包括建立新类型、限制在类型上的操作,束缚对象值以及消除客户的资源管理责任
条款19:设计class犹如设计type

你应该带着“语言设计者当初设计语言内置类型时”一样的谨慎来研讨每一个class的设计。因为,重载函数和操作符、控制内存的分配和归还、定义对象的初始化和终结…全都在你手上。

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

    设计函数为类的构造函数和析构函数以及内存分配函数和释放函数(见条款49-52)。

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

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

  • type的对象被passed by value,意味着什么?

    取决于copy构造函数的实现。

  • 什么新type的合法值?

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

    见条款73436

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

    explicitoperator关键字的使用。见条款15

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

    见条款232446

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

    见条款6

  • 谁该取用新type的成员?

    决定了数据成员是publicprivate还是protected,以及friend关键的使用。

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

    见条款29

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

    决定class template的使用。

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

条款20:宁以pass by reference to const替换pass by value
  • 当把具有继承关系的类作为参数传递时,如果pass by value可能会出现“截断”问题。
条款21:必须返回对象时,别妄想返回其reference
  • 绝不要返回指向一个local stack对象的pointerreference
  • 绝不要返回指向一个heap allocated对象的reference
  • 除非有单例模式的设计要求,否则绝对不要返回指向一个local static对象的pointerreference
条款22:将成员变量声明为private
  • 客户访问数据的一致性

    public里都是函数。

  • 可以对成员变量有更精确的控制

    可以实现成员变量的不可访问,只读、只写、读写访问。

  • 封装性

    将成员变量隐藏在函数接口背后,可以为所有可能的实现提供弹性。

  • 从封装的角度看,只有两种权限:private(提供封装)和其它

    protected并不比private更具封装性。

条款23:宁以non-membernon-friend替换member函数

考虑一个用来表示网页浏览器的class,这个class提供的众多成员函数中,有用来清除下载元素高速缓冲区的,有用来清除访问过的历史记录的,有用来移除系统中所有cookies的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class WebBrowser
{
public:
...
void clearCache();
void clearHistory();
void cleatCookies();
...
};

// 客户想一整个执行所有的操作

// 以提供一个 member 函数的方式
class WebBrowser
{
public:
...
void clearEverthing();
...
};

// 以提供一个 non-member 函数的方式
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.cleatCookies();
}

后者比较好。因为:

  • 增加封装性

    non-member non-friend将提供较大的封装性,因为它并不增加能够访问class内的private成分的函数数量。friend函数对class private成员的访问权力和member函数相同,两者对封装的冲击力度是一样的。

  • 增加扩充机能性

    C++中,正确且自然的做法是:

    1
    2
    3
    4
    5
    6
    namespace WebBrowserStuff
    {
    class WebBrowser {...};
    void clearBrowser(WebBrowser& wb); // 为 WebBrowser “提供便利”的函数
    ...
    }
    • namespaceclass的不同是,前者可跨越多个源码文件而后者不能。

      WebBrowser这样的类可能有大量的“提供便利”函数,某些与书签有关,与打印有关,与cookie有关… 分离它们最直接的做法就是:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // 头文件 WebBrowser.h 内
      namespace WebBrowserStuff
      {
      class WebBrowser {...};
      ... // 核心机能, 例如所有客户都想要的 non-member 函数
      }

      // 头文件 WebBrowserBookmarks.h 内
      namespace WebBrowserStuff
      {
      ... // 与书签相关的 non-member 函数
      }

      // 头文件 WebBrowserCookies.h 内
      namespace WebBrowserStuff
      {
      ... // 与 cookie 相关的 non-member 函数
      }

      将所有“便利函数”放在多个头文件内但隶属于同一个命名空间,意味客户可以轻松扩展这一组“便利函数”。

条款24:若所有参数皆需类型转换,请为此采用non-member函数
  • 有理数类Rational的实现

    重点关注用来将两个有理数相乘的operator*操作符重载函数的实现方式。

条款25:考虑写出一个不抛异常的swap函数

标准程序库提供的swap算法的典型实现为:

1
2
3
4
5
6
7
8
9
10
namespace std
{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}

只要T类型支持copying函数(copy构造函数和copy assignment操作符)。对于用户自定义类型,效率低下(需要三次复制)。

现在考虑所谓的pimpl(pointer to implementation)实现手法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 注意这是个模板类
template<typename T>
class WidgetImpl
{
public:
... // 细节不重要
private:
... // 有很多数据, 意味着复制时间很长
};

// 注意这是个模板类
template<typename T>
class Widget
{
public:
Widget(const Widget<T>& rhs);
// 关于 operator= 的一般性实现参考条款 10, 11, 12
Widget& operator=(const Widget<T>& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
private:
WidgetImpl<T>* pImpl; // 指向实际实现的对象
};

一旦需要置换两个Widget对象值,我们实际唯一需要的是置换两个指针即可。但缺省的swap函数不知道这一点!

下面是正确的实现步骤:

  1. Widget实现一个名为swappublic成员函数做真正的置换工作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<typename T>
    class Widget
    {
    public:
    ...
    void swap(Widget<T>& other)
    {
    using std::swap; // 必须的
    swap(pImpl, other.pImpl); // 直接置换指针即可
    }
    ...
    }
  2. std::swap特化,令它调用该成员函数(当Widget是类时。这里的例子中,Widget是个模板类则不要这一步)

    • 因为,C++只允许对class template偏特化,不允许对function template进行偏特化

      1
      2
      3
      4
      5
      6
      7
      8
      namespace std
      {
      template<>
      void swap<Widget>(Widget& a, Widget& b)
      {
      a.swap();
      }
      }
  3. 声明一个non-member swap,令它调用member swap

    1
    2
    3
    4
    5
    6
    // 注意, 要将它放在和 Widget<T> 同一个空间内(全局或者自定义的 namespace)
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
    a.swap(b);
    }
  4. 编程时,在调用swap置换对象的语句之前加上using std::swap声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 客户代码
    template<typename T>
    void doSomething(T& obj1. T& obj2)
    {
    using std::swap; // 令 std::swap 在此函数内可用
    ...
    swap(obj1, obj2); // 这样一来, 编译器将为 T 类型对象调用最佳版本 swap
    ...
    }

    编译器首先在全局作用域或T所在命名空间内寻找T(在这里,T就是例子中的Widget哦)专属的swap(也就是步骤3中实现的)。如果没有实现这些,则调用std内的swap,如果步骤2还实现了特化版本,将会选中特化版本。

  • 劝告,member swap绝不可抛出异常

    具体参考条款29。