0%

4 设计与声明

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

  1. 除非有好理由,否则应该尽量令你的types的行为与内置types一致。“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  2. “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除用户的资源管理责任。
  3. std::shared_ptr支持定制型删除其(custom deleter)。这可防范DLL问题(在这个dll创建对象,在另一边dll删除),可被用来自动解除互斥锁等等。

条款19:设计class犹如设计type

  1. C++程序员许多时间主要用来扩张类型系统(type system)。这意味着他不仅是class设计者,还是type设计者。包括重载(overloading)函数和操作符、控制内存的分配和归还、定义对象的初始化和终结……所以应该带着和“语言设计者当初设计语言内置类型时”一样的谨慎来研讨class设计。
  2. 设计高效的classes需要考虑:
    1. 新type的对象应该如何被创建和销毁?(operator new,operator new[],operator delete和operator delete[]的设计);
    2. 对象的初始化和对象的复制有什么样的差别?(构造函数和复制(assigment)操作符的行为;
    3. 新type的对象如果被passed by value(以值传递),意味着什么?
    4. 什么是新type的“合法值“? class必须维护的约束条件(invariants)(构造函数、赋值操作符和”setter“函数需要的错误检查工作。它影响函数的异常抛出、以及函数异常明细列(exception specifications)。
    5. 新type需要配合某个继承图系(inheritance graph)吗? (必然受到那些classes设计的束缚)
    6. 新type需要什么样的转换? (当需要隐式转换,必须写一个类型转换函数或者写一个non-explicit-one-argument(可被单一实参调用)构造函数)
    7. 什么样的操作符和函数对此新type而言是合理的?
    8. 什么样的标准函数应该驳回? (private)
    9. 谁该取用新type的成员? (public、protected、private、friends)
    10. 什么是新type的”未声明接口“(undeclared interface)?
    11. 新type有多么一般化? 如果是定义一整个types家族,就需要定义一个class template)
    12. 真的需要一个新type吗?

条款20:宁以pass-by-reference-to-const替换pass-by-value

  1. 当一个函数以pass by value方式接受对象,它的成本是很高的,很短的声明周期使它调用构造函数和析构函数。以by reference方式传递参数可以避免slicing(对象切割)问题(derived class对于base class来说的特化信息丢失)。
  2. 当使用内置类型有机会选择采用pass-by-value或pass-by-reference-to-const时,by value方式可能会效率高些,因为内置类型都相当小。一般可以合理假设”pass-by-value并不昂贵“的唯一对象就是内置类型和STL的迭代器和函数对象。

条款21:必须返回对象时,别妄想返回其reference

  1. 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个eap-allocated对象,或返回pointer或reference指向一个local sattic对象而有可能同时需要多个这样的对象。

条款22:将成员变量声明未private

  1. 成员变量的封装性与”成员变量的内容改变时所破坏的代码数量“成反比。取消一个public成员变量,所有使用它的客户码都会被破坏,;取消一个protected成员变量,所有使用它的derived classes都会被破坏。从封装的角度来看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
  2. 将成员变量声明为private。可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
  3. protected并不比public更具封装性。

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

  1. 如果某些东西被封装,越多东西被封装,就越多的弹性去改变它。
  2. 如果在一个member函数和一个non-member,non-friend函数之间做抉择,两者提供相同机能,那么较大封装性的是non-member non-friend函数,因为它不增加访问private成分的函数数量。
  3. 标准程序库并不是拥有单一、整体、庞大的<C++StandardLibrary>头文件并在其中包含std命名空间内的每一样东西,而是有数十个头文件,每个头文件声明std的某些机能。
  4. 将所有便利函数放在多个头文件但隶属同一个ing名空间,意味着客户可以轻松扩展这一组便利函数。它需要做的就是添加更多non-member non-friend函数到此命名空间内。

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

1
2
3
4
5
6
7
8
9
10
class Rational
{
public:
Rationalint numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
const Rational oeprator*(const Rational &rhs) const;
private:
...
};

当尝试混合式算式,只有一般行的通;

1
2
3
4
5
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // ok
result = oneHalf * 2; // ok
result = 2 * oneHalf; // error

oneHalf是一个内含operator *函数的class对象,而整数2并没有相应的class,也就没有operator*成员函数。但为什么第二个参数是2时可被接受?这里发生了隐式类型转换implicit type conversion)。

1
2
const Rational temp(2);
result = oneHalf * temp;

而实际想要支持混合式算数运算,就让operator*称为一个non-member函数,允许编译器在每一个实参身上执行隐式类型转换:

1
2
3
4
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
return Rational(lhs.numberator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

因此结论:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。

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

  1. 一旦要置换两个类对象值,唯一需要做的就是置换其pImpl指针,但default swap算法不知道这点。确切实践思路的一个做法是将std::swap针对该class对象特化total template specialization):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Widge
    {
    public:
    ...
    void swap(Widget &other)
    {
    using std::swap;
    swap(pImpl, other.pImpl); // 置换指针
    }
    };

    namespace std
    {
    temlate<>
    void swap<Widget>(Widget &a, Widget &b)
    {
    a.swap(b);
    }
    }
    假设WidgetWidgetImpl都是class templates而非classes,可以尝试将它们的数据类型参数化:
    1
    2
    3
    4
    5
    template<typename T>
    class WidgetImpl { ... };

    template<typename T>
    class Widget { ... };
    在类内放个swap成员函数很简单,却在特化std::swap时遇上乱流:
    1
    2
    3
    4
    5
    6
    7
    namespace std
    {
    // error
    template<typename T>
    void swap< Widget<T> >(Widget<T> &a, Widget<T> &b)
    { a.swap(b); }
    }
    看起来合理但不合法,这是企图偏特化partially specialize)一个function template,但C++只允许对class template偏特化。有时候std的内容是标准委员会决定,如果希望软件有预期行为,最好不加新东西到std里头。为此,还是声明non-member swap让它调用member swap,但不再将non-member swap声明为std::swap特别版本或重载版本。
    顺带一提,任何地点的任何代码打算置换两个Widget对象,因而调用swap,C++的名称查找法则(name lookup rules,或argument-dependent lookup和koenig lookup法则)会找到专属版本,那正是我们需要的:
    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T>
    void doSomething(T &obj1, T &obj2)
    {
    using std::swap; // 令std::swap在此函数可用
    ...
    swap(obj1, obj2); // 寻找最佳swap版本woc~
    ...
    }
    查找法则首先寻找global作用域或T所在命名空间内的T专属swap,找不到才使用std内的swap,这得感谢using声明式在函数内曝光。
  2. 成员版swap绝不可抛出异常,因为swap的一个最好的应用是帮助clasees或class templates提供异常安全性保障。