Effective C++ 笔记 (二)
目录

四. Designs and Declarations

18. Make interfaces easy to use correctly and hard to use incorrectly

如返回指向对象的指针,很可能使用户忘记删除指针所指对象。 所以不如直接返回tr1::shared_ptr<Class>类型的指针。

19. Treat class design as type design

20. Prefer pass-by-reference-to-const to pass-by-value

使用reference-to-const方式传递class类型参数能够:

  • 省去值传递中拷贝构造的代价
  • 避免slicing(对象切割)问题

而选择pass-by-value的对象可以是:

  • 内置类型
  • STL中迭代器和仿函数

编译器中,reference是实现往往还是依赖指针。

21. Don't try to return a reference when you must return an object

  • 不要返回pointer或reference指向local stack对象(内存错误)
  • 不要返回reference指向heap-allocated对象(无法删除)
  • 不要返回pointer或reference指向local static对象(线程或逻辑错误)

22. Declare data members private

封装,隐藏细节。

23. Perfer non-member, non-friend functions to member functions

封装,越少member看到数据,越多数据可以封装,就有越多的弹性。

因此避免冗余增加访问数据的member,而是变成non-member,non-friend, 把它们放在与class相同的命名空间下。

并且命名空间可以跨越多个源码,便于依赖解偶,这也是C++标准库组织形式。

24. Declare non-member functions when type conversions should apply to all parameters

member函数的反面是non-member函数,而不是friend函数。

25. Consider support for a non-throwing swap

传统的swap函数:

template<typename T>
void swap(T &a, T &b)
{
T temp(a);
a = b;
b = temp;
}

只要T支持拷贝构造和赋值操作符即可。 但是这种方法效率不高,我们可以针对具体的类设计高效的swap操作,作为member函数:

class A{
void swap(A &other)
{...} // 高效的交换
};

然后把std::swap针对类A做特化,并插入到std命名空间中(命名空间支持跨源码,并且标准允许为template特化版本)。

namespace std {
template<>
void swap<A>(A &a, A &b)
{
a.swap(b);
}
}

如果类是class template:

template<typename T>
class A{
...
};

那么以上方法是行不通的,因为C++只允许对class template偏特化,在function template身上偏特化是行不通的。只能是增加一个新的template函数(这里不是特化):

template<typename T>
void swap(A<T> &a, A<T> &b)
{
a.swap(b);
}

这个新的模版函数就不能加入std命名空间中了(破坏了C++标准,可以特化template,不能添加新的template,function,class)。所以可以把这个template函数加入到用户命名空间中。

最后,作为成员函数的swap不能抛出异常。


五. Implementations

26. Postpone variable definitions as long as possible

因为对象构造的代价,因此尽量真正用到对象时才构造。

循环体内用到的对象有两种定义方式:

  • 循环体外:这时对象的使用成本是1次构造+1次析构+n次赋值
  • 循环体内:这时对象的使用成本是n次构造+n次析构

要比较那种成本比较低。

27. Minimize casting

转型(casts)破坏了类型系统(type system)。

更多见C++ 类型转换

28. Avoid returning "handles" to object internals

handles:引用、指针和迭代器。

29. Strive for exception-safe code

异常安全性(Exception safety):当异常被抛出时,带有异常安全性的函数会:

  • 不泄露任何资源
  • 不允许数据破坏

异常安全函数(Exception-safe functions)提供以下三个保证之一:

  • 基本保证:异常被抛出时,程序内任何事物仍然保持在有效状态下。
  • 强烈保证:异常被抛出时,程序状态不改变。
  • 不抛出(nothrow)保证:函数不抛出异常。

可以使用“copy-and-swap”方法,或pimpl的方法(agent class,shared_ptr + impl class)。

异常安全有时可以满足强烈保证,但有时无法避免side effects。

木桶原理:函数提供的异常安全性等级取决于调用的各个函数中等级最低的。

30. Understand the ins and outs of inlining

  • inline 只是对编译器的申请,不是强制命令。大多数编译器拒绝太过复杂(如循环和递归)的inline。

  • 在类中定义的函数隐含也是inline申请。virtual函数绝大部分不会inline(因为是运行时决定调用)。

  • inline 可能增加目标程序的大小,导致代码膨胀问题,进而导致运行程序时更多的换页(paging)行为,降低指令Cache命中率。

  • inline 函数定义通常放在头文件内,因为大多数编译器是在编译期进行inlining,而不是在linking阶段。

  • inline 函数无法随着程序库的升级而升级。

  • 调试时禁止inline。

31. Minimize compilation dependencies between files

常规的C++类定义中不仅包含函数接口,而且包含数据实现。 因此更改数据实现部分(而不是平时说的函数实现)将导致依赖这个类的文件全部重新编译。 这是因为template的使用和编译时须知对象大小这两个原因造成的。

解决的方法是使代码依赖类的class声明式(class前置声明,class A;),而不是class定义式(class A{...};)。 object的references和pointers就可以只依靠class声明式; 但如果定义某class的objects,或访问class的成员,就需要该class的定义式(定义->申请对象空间->从而需要知道对象大小)。 同样,函数的声明式也不需要函数定义式,只需要函数声明式即可。 因此,基于以上两点,可以为声明式和定义式提供不同的头文件,如标准库中头文件。

基于此构想的两个降低依赖性的手段是:

  • Handle Class:对于目标A类,定义一个handle类只包含指向A类的指针,并wrap A类接口。这也称为pimpl(pointer to implementation)方法。代价是增加了访问的间接性。
  • Interface Class:对于目标A类,定义一个interface类声明其接口。通过Override方法运行时动态绑定。代价是动态绑定的代价。

发表评论