资源管理
条款13:以对象管理资源(Use objects to manage resources.),也就是“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization,RAII)
1)标准程序库提供的auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。
2)对对象管理资源的两个关键想法:(1)获得资源后立刻放进管理对象内;(2)管理对象运用析构函数确保资源被释放。
3)auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!也就是受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它。
4)auto_ptr的替代方案是“引用计数型智慧指针”(reference-counting smart pointer,RCSP),所谓RCSP也是智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收(garbage collection),不同的是RCSPs无法打破环状引用。TR1的tr1::shared_ptr就是个RCSP。
5)auto_ptr和tr1::shared_prt两者都在其析构函数内做delete而不是delete[]动作,就意味着在动态分配而得的array身上使用auto_ptr和tr1::shared_prt是个馊主意。
条款14:在资源管理类中小心copying行为(Think carefully about copying behavior in resource-managing classes.)
1)当一个RAII对象被复制时,可以采取下面几种措施:
(1)禁止复制。
(2)对底层资源祭出“引用计数法”。tr1::shared_ptr允许指定所谓的“删除器”,那是一个函数或函数对象,当引用次数为0时便被调用。
(3)复制底层资源。
(4)转移底层资源的拥有权。
2)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
条款15:在资源管理类中提供对原始资源的访问(Provide access to raw resources in resource-managing classes.)
1)tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,返回智能指针内部的原始指针(的复件)。
2)就像(几乎)所有智能指针一样,tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针。
3)对原始资源的访问可能经由显示转换或隐式转换,一般而言显示转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取相同形式(Use the same form in corresponding uses of new and delete.)
1)当通过new动态生成一个对象,有两件事发生。第一,内存被分配出去(通过名为operator new的函数)。第二,针对此内存会有一个(或更多)构造函数被调用。当使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。
条款17:以独立语句将newed对象置于智能指针(Store newed objects in smart pointers in standalone statements.)
1)以独立语句将newed对象存储于(置入)智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
设计与声明
条款18:让接口容易被正确使用,不易被误用(Make interfaces easy to use correctly and hard to use incorrectly.)
1)首先必须考虑客户可能做出什么样的错误。
2)明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。
3)限制类型内什么事可做,什么事不能做。
4)tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时将被调用的“删除器”。
条款19:设计class犹如设计type(Treat class design as type design.)
class的设计规范关乎的问题:
1)新type的对象应该如何被创建和销毁?这会影响class的构造函数和析构函数以及内存分配函数和释放函数(operator new,operator new[],operator delete和operator delete[])的设计。
2)对象的初始化和对象的赋值该有什么样的差别?这决定了构造函数和赋值(assignment)操作符的行为,以及期间的差异。别混淆“初始化”和“赋值”,对应于不同的函数调用。
3)新type的对象如果被passed by value(以值传递),意味着什么?copy构造函数用来定义一个type的pass-by-value该如何实现。
4)什么是新type的“合法值”?对class的对象变量而言,通常只有某些数值是有效的。那些数值集决定了class必须维护的约束条件(invariants),也就决定了成员函数(特别是构造函数、赋值操作符和所谓“setter”函数)必须进行的错误检查工作。它也影响函数抛出的异常、以及(极少被使用的)函数异常明细列(exception specifications)。
5)新type需要配合某个继承图系(inheritance graph)吗?如果继承自某些既有的classes,就受那些classes的设计束缚,特别是受到“它们的函数是virtual或non-virtual”的影响。如果允许其他classes继承你的class,那会影响你所声明的函数—尤其是析构函数—是否为virtual。
6)新type需要什么样的转换?如果希望允许类型T1之物被隐式转换为类型T2之物,就必须在class T1内写一个类型转换函数(operator T2)或在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。如果只允许explicit构造函数存在,就得写出专门复制执行转换的函数,且不得为类型转换操作符(type conversion operators)或non-explicit-one-argument构造函数。
7)什么样的操作符和函数对此新type而言是合理的?这决定class声明哪些函数。其中某些该是member函数,某些则否。
8)什么样的标准函数应该驳回?那些正是必须声明为private者。
9)谁该取用新type的成员?这决定哪个成员为public,哪个为protected,哪个为private。也决定哪一个classes和/或functions应该是friends,以及将它们嵌套于另一个之内是否合理。
10)什么是新type的“未声明接口”(undeclared interface)?它对效率、异常安全性(见条款29)以及资源运用(例如多任务锁定和动态内存)提供何种保证?在这些方面提供的保证将为class实现代码加上相应的约束条件。
11)新type有多么一般化?也许并非定义一个新type,而是定义一整个types家族,应该定义一个新的class template。
12)真的需要一个新type吗?
条款20:宁以pass-by-reference-to-const替换pass-by-value(Perfer pass-by-reference-to-const to pass-by-value.)
1)这种传递方式效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。
2)以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。
解决切割(slicing)问题的办法,就是以by reference-to-const的方式传递derived class。
3)一般而言,可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL的迭代器和函数对象。
条款21:必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object.)
1)所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,都应该对它的另一个名称提出疑问。
2)任何函数如果返回一个reference指向某个local对象,都将一败涂地。如果函数返回指针指向一个local对象,也是一样。
3)一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象呗。
4)绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明为private(Declare data members private.)
1)成员变量应该是private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充实的实现弹性。
2)使用函数可以让你对成员变量的处理有更精确的控制。如果令成员变量为public,每个人都可以读写它,但如果你以函数取得或设定其值,你就可以实现出“不准访问”、“只读访问”以及“读写访问”,甚至可以实现“唯写访问”。
3)封装。如果通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。
4)protected并不比public更具封装性。
条款23:宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions.)
1)如果要在一个member函数(它不只可以访问class内的private数据,也可以取用private函数、enums、typedefs等等)和一个non-member,non-friend函数(它无法访问上述任何东西)之间做抉择,而且两者提供相同机能,那么,导致较大封装性的是non-member non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量。
2)将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。他们需要做的就是添加更多non-member non-friend函数到此命名空间内。
3)宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
条款24:若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters.)
1)只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”——即this对象——的那个隐喻参数,绝不是隐式转换的合格参与者。
2)如果需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
条款25:考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap.)
1)典型swap算法如下图所示:
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
只要类型T支持coping(通过copy构造函数和copy assignment操作符完成),缺省的swap实现代码就会帮你置换类型为T的对象,你不需要为此另外再做任何工作。
2)“pimpl手法”(pimpl是“pointer to implementation”的缩写)是指“以指针指向一个对象,内含真正数据”这种类型的表现形式。如Widget class:
class WidgetImpl{ //针对Widget数据而设计的class,细节不重要。
public:
...
private:
int a, b, c;
std:vector<double> v; //可能有很多数据,意味复制时间很长。
...
};
class Widget{ //这个class使用pimpl手法
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) //复制Widget时,令它复制其WidgetImpl对象
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl* pImpl; //指针,所指对象内含Widget数据。
};
我们希望能够告诉std::swap,当Widgets被置换时真正该做的是置换其内部的pImpl指针。一个思路是:将std::swap针对Widget特化。令Widget声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数。
class Widget{
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
...
};
namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
3)假设Widget和WidgetImpl都是class templates而非classes。C++只允许对class templates偏特化,而function templates身上偏特化是行不通。当打算偏特化一个function template时,惯常做法就是简单为它添加一个重载版本。
namespace std{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
但std是个特殊的命名空间,其管理规则也比较特殊,客户可以全特化std内的templates,但不可以添加新的templates(或classes或functions或其他任何东西)到std里头。很简单,将swap置于Widget所在的命名空间中,如下所示:
namespace WidgetStuff{
...
template<typename T>
class Widget{ ... };
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
4)C++的名称查找法则,如下所示:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; //令std::swap在此函数内可用
...
swap(obj1, obj2); //为T型对象调用最佳swap版本
...
}
C++的名称查找规则(name lookup rules)确保找到global作用域或T所在命名空间内的任何T专属swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using声明式让std::swap在函数内曝光。
5)首先,如果swap的缺省实现版对你的class或class template提供可接受的效率,你不需要额外做任何事。
其次,如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:
(1)提供一个public swap成员函数,让它高效的置换你的类型的两个对象值。
(2)在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
(3)如果你正编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数。
最后,如果你调用swap,请确定包含一个using声明式,以便std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
6)成员版swap绝不可抛出异常。因为swap的一个最好应用是帮助classes(和class templates)提供强烈的异常安全性(exception-safety)保障。