0%

More Effective C++ 读书笔记(二)

本文包含《More Effective C++》中条款9至条款27内容的个人笔记。

条款9:利用destructors避免泄露资源

通过使用智能指针或是将资源封装在栈上的对象内做到资源自动释放,但此种做法应该仅适用于异常抛出在非构造和析构时抛出;

条款10:在constructors内阻止资源泄露(resource leak)

如果constructor内出现可能抛出异常造成资源泄露的情况,需要在构造内做相应的异常捕获及资源管理;一种较好的管理方式是由智能指针去以对象的方式管理资源,做到在不引用时自动释放;

条款11:禁止异常(exceptions)流出destructors之外

析构抛出异常易造成相应资源未释放而异常已抛出,或是在外层调用本身是在有异常作用下,造成程序异常退出;因此,禁止异常流出析构,相应做法catch后不向外抛出;

条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异

  1. exception objects总是会被复制,如果以by value方式捕捉,它们甚至会被复制两次;
  2. “被抛出成为exceptions”的对象,其被允许的类型转换动作,比“被传递到函数去“的对象少
  3. catch子句以其”出现域源代码的顺序“被编译器检验比对

条款13:以by reference方式捕捉exceptions

以by point传递捕获exception易造成是否该删除该指针所指的问题,以by value传递捕获指针将造成exception对象复制两次的消耗;以by reference传递捕获指针将避免出现以上问题,同时不会出现by value传递时异常对象的虚函数调用无法准确命中的问题;

条款14:明智运用exception specifications

  1. 不要对template方法做exception specification,不同的类型可能重载不同的操作符函数,而无法保证这些操作符函数不抛出exception或是抛出何种exception;
    *这点在C++11标准中有了新的方法,noexcept关键字加入,确认类型是否为不抛异常类型有了新的方式,在编译器模板特殊化时异常可有保证
    https://msdn.microsoft.com/zh-cn/library/dn956976.aspx
  2. 如果该方法调用了其他方法,而这些其他方法如果未做exception specification,那么调用这些其他方法的函数亦不应该做exception specification,原因显而易见;
    *这里书上有提到一个c++标准功能,声明函数指针类型时做exception specification,在不同compiler上试了下,g++ 4.8.2编译报错"declared with an exception specification",应该时不支持此种声明;vs2015下,如下做声明
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef void (* nothrow_func)() throw();

void test_func() throw (...)
{
throw (1);
}

int main()
{
//...
nothrow_func p_nothrow = test_func;
//...
}

编译未见任何error,warning,有关这点我认为是C++11标准发布后,exception specification被认为是不太合适的语言特性而被弃用,导致此类编译器bug未被重视;
附MSDN上对于异常规范的连接:
https://msdn.microsoft.com/zh-cn/library/wfa0edys.aspx
其三,由于exception specification很多时候不能保证不抛出其他未声明的异常,所以只能通过set_unexpected方法改写unexpected方法,在unexpected方法中去重新抛出可捕获异常防止程序调用terminate;

*这里书上对于exception specification也是持中立态度,C++11标准中exception specification保留了throw()特性,增加了noexcept关键字,保留了保证不抛出异常这个比较有用的语言特性,剔除了throw(XXX)这个特性,其实也是因为exception specifications大多时候并不能保证其他类型的异常的抛出

条款15:了解异常处理(exception handling)的成本

异常对于程序来说是会有额外的时间和空间开销的,这里书上大致说了个数字5%~10%(时间和空间皆为),当然这应该是针对早期的某个编译器来说;try语句块和exception specification会给程序带来额外的开销,应在必要时使用;
*这里查了下MoreEffectiveC++的著成时间,为1995年,所以这里对于C++的编译器的某些观点也许会有所过时,异常处理对于程序的额外开销稍后写些东西来测试下;

条款16:谨记80-20法则

80%的消耗在20%的代码上,利用profiler找到20%的代码去改变这20%代码的效率可有效提高程序效率;

条款17:考虑使用lazy evaluation(缓式评估)

缓式评估是一种思路,比如在构造阶段需要计算一个来自两个矩阵参数相乘的结果时,如果这个结果并不是急于使用,可以先不做计算而放在取矩阵的相应方法里去计算;但是lazy evaluation作法是需要根据应用场景来判断是否该使用的,比如上面的例子中,如果矩阵结果急于使用,lazy evaluation则并不能给程序效率上带来什么帮助;

条款18:分期摊还预算的计算成本

eager evaluation
lazy evaluation
over-eager evaluation
当你必须支持某些运算而其结果并不总是需要的时候,lazy evaluation可以改善程序效率;
当你必须支持某些运算而其结果几乎总是被需要,或其结果常常被多次需要的时候,over-eager evaluation可以改善程序效率;

条款19:了解临时对象的来源

匿名临时对象差生通常发生于两种情况,一是当隐式类型转换(implicit type conversions)被施行起来以求函数调用能够成功;二是当函数返回对象的时候;
reference-to-const参数和返回会产生临时对象,因其不可更改,编译器认为传递一个不可更改的临时变量地址不会出现问题;
reference-to-none-const参数和返回不会产生临时对象,因其可更改,编译器认为传递一个可更改的临时变量是危险的;

条款20:协助完成"返回值优化(RVO)"

return value optimization简称RVO,当函数需要返回一个对象时,如果是命名对象,将发生临时命名变量的构造,返回变量的复制构造,临时命名变量的析构三个操作,而RVO支持如果返回的是一个临时匿名变量时,将直接在返回变量上做构造,省去复制构造和析构的步骤;这里侯捷在书上附注了一条ISO/ANSI对于RVO的补充说明,注明命名临时变量也应支持RVO,编译器应支持对命名变量的RVO优化,但我使用vs2015测试后发现命名变量并没有得到RVO的支持;

条款21:利用重载技术(overload)避免隐式类型转换(implicit type conversions)

为自定义类型添加多种重载函数接口,避免在调用时编译器自动执行隐式类型转换,造成不必要的开销。

条款22:考虑以操作符复合形式(op=)取代其独身形式(op)

复合形式操作符由于不产生临时对象,因此效率上比产生临时对象的独身形式操作符高;

条款23:考虑使用其他程序库

该条款意在拓宽思路,针对某个功能可能有多个程序库同时提供该功能,但各个程序库设计上考虑的重点不同,可能有的优先于性能有的优先于方便使用;当程序库的性能成为程序瓶颈时,不妨使用另一具有更高性能的程序库来打破,该条款同时还提到可以使用benchmark软件来对各个程序库来进行测试。

条款24:了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本

虚函数列表,虚函数列表指针在对象大小很小时,会格外显得占用内存空间

条款25:将constructor和non-member functions虚化

所谓virtual constructor是某种函数,视其获得的输入,可产出不同类型的对象。这里提到了一种特殊的virtual copy constructor,即本身不是copy constructor但是执行的操作为复制构造。
non-member functions虚化方法,写一个虚函数做实际工作,再写一个负责调用虚函数的non-member function的非虚函数。
例如现在需要使用ostream<<输出一个继承体系类型的值,只需在这个继承提醒内定义一个虚函数print负责打印各个层次的值,然后再写一个operator<<(ostream& os,cons xxx& c)的非成员非虚函数,接受祖先类引用,负责调用其print函数。

条款26:限制某个class所能产生的对象数量

允许产生零个或者一个对象
不允许外部产生对象很简单,只需将这个类的构造函数声明为private,即可防止外部产生新对象;
只产生一个对象,做法其一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PrintJob;
class Printer {
public:
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
...
friend Printer& thePrinter();
private:
Printer();
Printer(const Printer& rhs);
...
};

Printer& thePrinter()
{
static Printer p;
return p;
}

这里将public方法thePrinter声明为Printer的friend方法,使thePrinter方法可以不受private constructor约束;
以上是书上的说明,但是我使用g++ (GCC) 4.4.7编译以下代码时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
class Test
{
public:
static Test& theTest()
{
static Test test;
return test;
}
int m_iInt;
private:
Test() : m_iInt(1)
{}
};
int main()
{
Test test = Test::theTest();
cout << test.m_iInt << endl;
return 0;
}

编译器未报错,且运行正常,说明当前版本的编译器中对public函数并未有不能使用private函数的限制;

不同的对象构造状态

1
2
3
4
5
6
7
8
9
10
11
12
class FSA {
public:
static FSA * makeFSA();
static FSA * makeFSA(const FSA& rhs);
private:
FSA();
FSA(const FSA& rhs);
};
FSA* FSA::makeFSA()
{ return new FSA(); }
FSA* FSA::makeFSA(const FSA& rhs)
{ return new FSA(rhs); }

允许对象生生灭灭

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
29
class Printer {
public:
class TooManyObjects{};
//pseudo-constructor
static Printer* makePrinter();
~Printer();
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
...
private:
static size_t numObjects;
Printer();
Printer(const Printer& rhs);
};

size_t Printer::numObjects = 0;

Printer::Printer()
{
if ( numObjects >= 1) {
throw TooManyObjects();
}
proceed with normal object construction here;
++numObjects;
}

Pinter* Printer::makePrinter()
{ return new Printer; }

一个用来计算对象个数的Base Class

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
29
template<class BeingCounted>
class Counted {
public:
class TooManyObjects{};
static int objectCount() { return numObjects; }
protected:
Counted();
Counted(const Counted& rhs);
~Counted() { --numObjects; }
private:
static int numObjects;
static const size_t maxObjects;
void init();
};

template<class BeingCounted>
Counted<BeingCounted>::Counted()
{ init(); }

template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{ init(); }

template<class BeingCounted>
void Counted<BeingCounted>::init()
{
if(numObjects >= maxObjects) throw TooManyObjects();
++numObjects;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Printer : private Counted<Printer> {
public:
static Printer* makePrinter();
static Printer& makePrinter(const Printer& rhs);
~Printer();
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
using Counted<Printer>::objectCount;
using Counted<Printer>::TooManyObjects;
private:
Printer()
Printer(const Printer &rhs);
};

条款27:要求(或禁止)对象产生于heap之中

要求对象产生于heap之中(Heap-Based Objects)
一种思路是将所有能够隐式调用类型构造和析构的地方进行禁止,既将类型的构造或析构声明为private,而由于构造函数有多个析构只有一个,所以方便的方式是将析构声明为private,另外声明一个public的pseude destructor,在这个伪析构里调用delete this将自身做析构和删除;考虑到继承inheritance和内含containment的情况,private的析构将阻止继承和内含,所以可以将析构声明为protected,在内含的形式中声明为指针;