原文链接:http://harttle.com/2014/05/04/effective-cpp.html
两年前第一次接触 Scott Meyers 的 Effective C++,最近面试C++工程师,又拿来研读,整理了一些读书笔记。
Prefer consts, enums, and inlines to #defines.
尽量使用常量、枚举和内联函数,代替#define
。即习惯于使用C++风格,可以避免模块化编程的诸多缺点。
#define
常量全局可用,不利于封装。因为在预编译时进行宏替换,与它们相关的编译错误将不会出现宏名称,不利于调试。enum
定义类常量较为合适。因为static const int
允许取地址,此时不仅需要声明,还需要定义。#define
函数将会产生出乎意料的结果,如#define MAX(a,b) a>b?a:b
,在调用MAX(i++,j)
的情况下,i
自加次数将取决于j
的大小。Use const whenever possible.
尽量使用常量。这在C++风格中是值得提倡的,尤其在合作项目中。不想改变的变量应该明确地提醒其他程序员,不要侥幸!
*
的运算符,应返回const
类型,来避免(a*b)=c
这样的语句通过编译。mutable
来使const函数能够更改logical常量(如缓存)。const 与 non-const 成员函数的避免重复。
const char& operator[](size_t pos) const{
...
}
char& operator[](size_t pos){
return const_cast<char&>(
static_cast<const TextBlock&>(*this)
[pos]
);
}
Make sure that objects are initialized before they’re used.
在使用对象前确保其已经初始化。出于效率原因,C++不保证内置型的初始化。
=
是赋值不是初始化!在构造函数前总是列出所有成员变量,以免遗漏。多个编译单元的 non-local static 对象初始化次序是不确定的。使用 Singleton(local static) 来解决该问题:
class FileSystem{...};
FileSystem& tfs(){
//将在首次进入函数时构造
static FileSystem fs;
return fs;
}
以下提供较完整的Signleton C++实现:
class Singleton{
private:
Singleton(){}
Singleton(const Singleton&);
public:
static Singleton& getInstance()
{
Lock(); // not needed after C++0x
static SingletonInside instance;
UnLock(); // not needed after C++0x
return instance;
}
};
注意:C++0X以后,要求编译器保证内部静态变量的线程安全性,可以不加锁。这里采用内部静态变量方式,还有另外两种实现:
new
)。new
),指针需要为const
。Know what functions C++ silently writes and calls.
如果没有定义任何构造函数(析构函数,复制构造函数,=
运算符),编译器会生成默认构造函数(析构函数,复制构造函数,=
运算符)。调用时机如下:
explicit
来避免这种情况)delete
=
运算符:一个对象赋值给另一对象Explicitly disallow the use of compiler-generated functions you do not want.
对于Uncopyable
,声明其复制构造函数与=
运算符为private
:
class HomeForSale{
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
Declare destructors virtual in polymorphic base classes.
对于多态用途的父类,析构函数应声明为virtual
,否则当指针或引用类型为父类时,调用析构函数将只析构父类中定义的资源。相应地,non-virtual
析构函数的类不应被继承。(很不幸,C++不提供类似Java的final class
或C#的sealed class
禁止来派生)。
Prevent exceptions from leaving destructors.
在析构函数中,应处理掉可能的异常,保证对象能够被完整地释放。为了让用户有机会处理这些异常,可以为不安全语句提供一个新的函数;在析构函数中执行默认操作(忽略、记录、或者结束程序)。
class DBConn{
public:
void close(){
db.close();
closed = true;
}
~DBConn{
if(!closed){
try{
db.close();
}
catch(...){
...
}
}
}
private:
DBConnection db;
bool closed;
};
Never call virtual functions during construction or destruction.
在子类对象的父类构造期间,对象类型为父类而非子类。此时,虚函数会被解析至父类,运行时类型信息也为父类(dynamic_cast
, typeid
)。
即父类构造期间,对虚函数的调用不会下降至子类。对于构造函数中直接的虚函数调用,某些编译器会发出警告。
Have assignment operators return a reference to *this.
这是关于赋值运算符的协议,以支持连锁赋值语句。
Handle assignment to self in operator=.
确保当对象自我赋值时,=
运算符仍有良好的行为。当然可以事先进行判断(是否为自我赋值),更为简洁的方式为:copy-and-swap.
// 注意这里为传值,而不是经典的常量引用
Widget& Widget::operator=(Widget rhs){
swap(rhs);
return *this;
}
Copy all parts of an object.
复制构造函数与=
运算符应拷贝对象的所有成员,这是显然的。然而在两种情况下可能发生疏忽:
=
运算符)。注意:虽然=
运算符与复制构造函数的行为很相似,但在=
运算符中调用复制构造函数时不合理的,就像是试图构造一个已经存在的对象。通常将相同的代码抽出来放入private init
方法中。
Use objects to manage resources.
使用new
构造的对象,如果将删除它的责任给调用者,将很容易发生疏漏,尤其是控制流发生改变时(无意的提前return
, break
, etc.)。
利用对象在作用域结束时自动的析构函数,可以实现资源的自动释放,即RAII(Resource Acquisition Is Initialization)。在std
下已经存在这样一个预定义的类模板:auto_ptr
,它被销毁时会自动删除所指之物,被赋值时原指针将会变为null来保证资源的唯一。
std::auto_ptr<Investment> pInv(createInvestment());
同样,std::tr1::shared_ptr
为RCSP(reference-counting smart pointer),类似垃圾回收(garbage collection),不同的是RCSP无法解决环状引用(cycles of references)。
另外,auto_ptr
与shared_ptr
在析构函数内均调用delete
而不是delete[]
,故不能用于数组的管理。数组可选用STL的容器。
Think carefully about copying behavior in resource-managing classes.
RAII对象的复制需要特殊处理,其复制行为由资源的复制行为决定。通常有如下几种:
Provide access to raw resources in resource-managing classes.
资源管理类当然需要提供API来提供原始资源。提供方式可能为隐式转换或显式转换,隐式转换更方便,但有可能会造成误用(混淆了RAII对象与原始资源),不够安全。
Use the same form in corresponding uses of new and delete.
这条为C++内存使用的规则。使用new
则对应使用delete
;使用new sth[]
则对应使用delete[]
。
尽量不对数组形式做typedef
来避免疏忽。
Store newed objects in smart pointers in standalone statements.
使用单独语句将资源置入智能指针。这样可以避免因异常而导致内存泄漏。如:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
C++的执行次序是未知的。如果先创建了对象,此后执行priority()
产生异常,则该资源会被泄漏。
Make interfaces easy to use correctly and hard to use incorrectly.
好的接口不容易被误用。“促进正确使用”包括:
const
,等等。cross-DLL problem:跨DLL成对使用new
和delete
容易引发运行时错误。使用智能指针可以消除这个问题,它会将引用记录一并带入新的环境中,在引用为0时调用delete
。
Treat class design as type design.
这里提出了定义新类时需要考虑的一系列问题:
Prefer pass-by-reference-to-const to pass-by-value
通常传递引用会更加高效,而且可以避免对象切割问题(子类传值给父类,将损失子类信息)。
对于并不昂贵的拷贝操作,传值更加高效,这些类型包括:
Don’t try to return a reference when you must return an object.
Declare data members private.
将数据成员声明为private
。这样可提供更好的一致性、精确的访问控制、更强的约束条件、充分的弹性(方便实现更改通知、条件赋值等)。
Prefer non-member non-friend functions to member functions.
更多采用非成员、非友元函数,而不是成员函数。好处如下:
Declare non-member functions when type conversions should apply to all parameters.
当所有参数都可能需要类型转换时,采用非成员函数。这通常用于运算符重载的情形:当重载为成员函数时,第一个操作数必须为*this
;重载为非成员函数时,第一个操作数可以为任何兼容的数据类型。
class Rational{
public:
...
const Rational operator* (const Rational& rhs) const;
}
Rational r(1,8);
Rational result1 = r * 2; //ok, 隐式类型转换
Rational result2 = 2 * r; //error!
const Rational operator*(const Rational& lhs, const Rational& rhs);
Rational result1 = r * 2; //ok, 隐式类型转换
Rational result2 = 2 * r; //ok, 隐式类型转换
Consider support for a non-throwing swap.
swap
是STL的一部分,是异常安全编程(exception-safe programming)的基础,以及用来处理可能的自我赋值。
std::swap
效率不高时,可以提供一个成员 swap
,并用非成员swap
调用它。std::swap
。对于类模板,因为C++不允许模板函数的偏特化,我们不能特化std::swap
。采用如下的方法:
namespace WidgetStuff{
...
template<typename T>
class Widget{...};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
template<typename T>
void dosth(T& obj1, T& obj2){
using std::swap; //使之可见
...
swap(obj1, obj2); //自动选择最佳
...
}
编译器会首先去模板参数类型所在命名空间寻找;因为前面的声明,std::swap
中的匹配特化版将有最高的优先级;如果前两者都不存在,则采用std
中的默认版本。
Postone variable definitions as long as possible.
局部变量的定义需要承受其构造与析构成本,即使是未被使用的变量。因此推迟变量定义可增加清晰度和改善效率。
对于循环中使用的变量,其时间成本如下:
另外,方法1还会降低可理解性和易维护性。因此,除非(1)你知道复制成本<构造+析构(2)这部分效率高度敏感(performance-sensitive),否则应使用方法2.
Minimize casting.
尽量别转型。C++的设计目标之一即是:杜绝类型错误。
我们回顾一下转型语法:
C风格的转型:
(T) expression
函数风格的转型:
T(expression)
这两者都属于旧式转型(old-styled casts)
C++ 新式转型:(new-style,C++-style casts)
// 常量性移除(cast away the constness)
const_cast<T>(expression)
// 安全向下转型(safe downcasting)
dynamic_cast<T>(expression)
// 低级转型,实际动作可能取决于编译器,不可移植
reinterpret_cast<T>(expression)
// 强制隐式转换(implicit conversions)
static_cast<T>(expression)
应尽量采用C++风格转型,因为(1)容易辨识(2)功能明确,编译器容易找到错误。当然有时采用旧式转型仍然很方便,比如将转型隐藏在函数调用中:
void doSomeWork(const Widget& w);
doSomeWork(Widget());
注意:
BaseClass::func()
或virtual func;
来实现此类功能。dynamic_cast
在很多实现版本中效率很差。一种实现是通过类名的strcmp
来得到子类。Avoid returning “handles” to object internals.
避免返回对象内部变量的句柄(包括指针、引用、智能指针)。这样既会破坏封装,也可能造成空悬指针。
破坏封装。返回私有成员和私有函数,无疑都会使其私有性形同虚设。以下是一个返回私有成员函数指针的例子。
class A;
typedef void (A::* pfunc)();
class A{
private:
void func(){
cout<<"I'm private."<<endl;
}
public:
pfunc get(){
return &A::func;
}
};
...
A a;
pfunc p = a.get();
(a.*p)();
上述问题表明:成员的封装性不大于返回其引用的函数的访问级别。另外,如果const
成员函数传出引用,而后者的数据存储于对象之外,则会造成常量性的破缺(bitwise constness)。
空悬指针。显然对象的内部变量一旦传出,其生存期就可能超过它的来源对象,空悬指针便会成为可能。这是应极力避免的。然而,在某些情况下却不得不这样做:
class string{
...
&char operator[](int i){
...
}
}
Strive for exception-safe code.
尽量实现异常安全。异常安全函数(Exception-safe functions)当异常抛出时:
异常安全函数应提供以下三个保证之一:
给一个异常安全性很差的例子:
// 当 new Image 抛出异常时
void changeBg(string imsSrc){
lock(&mutex);
delete bgImage; // bgImage 变为空,数据破坏
++imageChanges; // 不应进行的累加,数据破坏
bgImage = new Image(imgSrc);
unlock(&mutex); // 将不会释放互斥锁,资源泄漏
}
对于这个例子,我么可以采用自动锁、智能指针,并适当更改代码顺序,即可实现强烈保证。
void changeBg(string imgSrc){
Lock m1(&mutex); // 退出作用域将自动释放
bgImage.reset(new Image(imgSrc)); // reset只有其参数成功后才会调用
++imageChanges;
}
另外,对于实现异常安全存在一般的方法:先生成副本,对副本进行所有操作,结束后进行swap
。无疑这回增加性能负担。
void changeBg(string imgSrc){
using std::swap;
Lock m1(&mutex); // 退出作用域将自动释放
... // generate pNew from pOld
swap(pNew, pOld);
}
值得一提的是,函数提供的“异常安全保证”通常最高只等于其所调用的各个函数中最弱的“异常安全保证”。
Understand int ins and outs of inlining.
Inline函数的定义有两种方式:(1)显示的定义,在函数名前加inline
,(2)隐式的定义,在类声明中定义函数体,包括friend
函数和构造函数。
Inline只是对编译器的一个申请而非强制命令。在如下情形中,inline无效:
Inline函数的缺点:
构造函数看起来是inline函数的绝佳候选人,因为通常不含任何代码。但是,编译器会在编译前在此插入大量的代码以保证C++机制的实现。如:定义和new
时自动初始化,父类和成员对象自动初始化,退出作用域自动销毁,异常抛出时已构造好的部分自动销毁等等。
inline 一般位于头文件,因为编译时为了替换首先要得到其定义式;同样template也一般在头文件,同样为了具现化,首先要得到其定义式。但二者无特定关联。template函数如果没有理由就不应设为inline。
Minimize compilation dependencies between files.
最小化文件之间的编译依赖。C++没有提供很好的“接口与实现分离机制(public与private的分离)。最小化编译依赖的一般构想是:依赖于声明式,而不是定义式。如下做法都源自这样的策略:
有如下两种方法可以实现接口与实现的分离。
在Handle class和Interface class中,inline函数用来隐藏其实现细节(即函数本体)。
Make sure public inheritance models “is-a”.
public继承一定要用在is-a关系中,要满足李氏替换原则(Liskov Substitution Priciple)。比如鸟都会飞,企鹅继承自鸟,便是一种错误:
class Bird{
public:
virtual void fly();
};
class Penguin: public Bird{
};
考虑到只有部分鸟会飞,得到更好的实现:
class Bird{};
class FlyingBird: public Bird{
public:
virtual void fly();
};
class Penguin: public Bird{};
当然可以不实现一个Flyingbird
,而使Penguin::fly();
抛出一个错误。这样做的后果是将错误从编译器延迟到运行期。
Avoid hiding inherited names.
避免覆盖继承而来的成员。子类中的成员会覆盖父类的同名成员,而无视参数是否相同:
class Base{
void f();
void f(int);
};
class Derived: public Base{
void f();
};
Derived d();
d.f(3); // 错误!
可以采用using
声明式来使父类中的其他名称可用:
...
class Derived: public Base{
using Base::f; //暴露Base中名为f的所有东西
void f();
};
Derived d();
d.f(3); //现在可以了
或者forwarding函数调用父类的实现:
...
class Derived: public Base{
void f(){ Base::f(); } // inline forwarding function
};
Derived f();
d.f(3); //错误!同样被覆盖
Differentiate between inheritance of interface and inheritance of implementation.
区分接口继承和实现继承。
对于第2种情况,考虑到缺省实现只用于真正实现的部分(为了避免代码冗余),而真正的实现不应直接使用缺省实现的情况。可以定义pure virtual函数,并以另一个protected函数来提供缺省实现。一种更优雅(避免相似的函数名称造成命名空间污染)的实现,可以为pure virtual函数提供缺省实现:
class Airplane{
public:
virtual void fly()=0;
}
void Airplane::fly(){ // 编译器不会报错
... //缺省实现
}
class ModelA: public Airplane{
public:
virtual void fly(){
Airplane::fly(); // 纯虚函数实现的调用方式:pModelA->Airplane::fly()
... //自定义实现
}
}
这样既强制用户实现自己的方法,同时提供了缺省实现。
Consider alternatives to virtual functions.
当你寻找设计方法时,不妨考虑一下virtual函数的一些替代方案。如template method模式,Strategy模式。
采用NVI(Non-Virtual Interface)实现的Template method模式。由non-virtual函数(即为接口)作为模板方法,划分其执行步骤。virtual函数来提供其各步骤的实现方式:
class GameCharacter{
public:
int healthValue() const{
... // 前面的处理
int v = doHealthValue(); // 真正的工作
... // 后面的处理
return v;
}
private:
virtual int doHealthValue() const{} //由子类提供不同的实现
};
采用函数指针实现的Strategy模式。由函数指针来提供计算生命值的策略,这类方法中难免出现灵活性与降低封装的权衡。
int *defaultHealthCalc(const GameCharacter&);
class GameCharacter{
public:
typedef int (*healthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc): healthCalcFunc(hcf);
int healthFunc(*this) const{return healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
GameCharacter c1(loseHealthQuickly);
GameCharacter c2(loseHealthSlowly);
可以看到Strategy模式提供了很大的灵活性:(1)同一类型的对象可以有不同的计算策略,(2)计算策略可以在运行时变更。另外,如果采用tr1::functoin
代替函数指针将提供更大的灵活性:计算策略可以包括兼容的函数、函数对象、成员函数等等。
...
class GameCharacter{
public:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
...
};
short calcFunc(const GameCharacter); //兼容的函数
struct Calculator{
int operator(const GameCharacter&) const{...}; // 函数对象
};
class GameLevel{
public:
float health(const GameCharacter&) const; //成员函数
};
GameCharacter c1(calcFunc);
GameCharacter c2(Calculator());
GameLevel currentLevel;
GameCharacter c3(
std::tr1::bind(&GameLevel::health,
currentLevel,
_1); // 以currentLevel调用health函数
Never redefine an inherited non-virtual function.
不要覆盖父类的non-virtual函数。不同于vitual函数的动态绑定(dynamically bound),non-virtual函数是静态绑定(statically bound)的。通过B类型的指针调用的non-virtual函数永远是B内定义的版本。这会造成如下的不一致:
D x;
D* pD = &x;
B* pB = &x;
pD->mf();
pB->mf(); //结果不同于上面的调用,违反is-a设计。
Never redefine a function’s inherited default parameter value.
不要重新定义继承来的方法的默认参数。对于继承来的non-virtual函数,根本不应该覆盖(如上一条);对于继承来的virtual函数,vitual函数是动态绑定的,而其参数则是静态绑定。即:使用父类指针调用子类中定义的虚函数,其默认参数却永远来源于父类。这是C++基于编译器实现的简易度和程序运行效率的取舍。
Model “has-a” or “is-implemented-in-terms-of” through composition.
在应用域(application domain),复合意味着 has-a;在实现域(implementation domain),复合意味着 is-implemented-in-terms-of。
Use private inheritance judiciously.
明智地使用私有继承。私有继承意味着implemented-in-terms-with,并且只继承实现。
通常,私有继承比组合的级别低,他们的区别在于private inheritance允许子类继续更改virtual函数,访问其protected成员。另外,在极端情况下,由于EBO(empty base optimization,空基类消耗空间为0,而作为独立对象大小为1,不考虑alignment)私有继承会稍节省空间。
Use multiple inheritance judiciously.
明智地使用多重继承。多重继承比单一继承复杂,而且可能导致歧义性。例如:
class A{
public:
void f();
};
class B{
public:
void f() const;
};
class D:public A, public B{};
D d;
d.f(); // 歧义,编译错
如上示例中,C++总是找出最佳匹配的名称后才检测其可用性(包括访问级别、参数、constness)。匹配程度相同便会造成歧义。另外,对于“钻石型”多重继承,子类会有多份祖父的数据。为了避免重复,必须将带有此数据的class成为virtual base class:令所有继承自它的class采用virtual继承(如class D: virtual public A{};
)。
多重继承还可以用来实现接口与实现的分离,即public继承接口类,private继承实现类。
Understand implicit interfaces and compile-time polymorphism.
Templates 和 Classes 同样都支持接口和多态。
Understand the two meanings of typename.
typename
和class
完全等价。typename
还用来标识嵌套从属类型名称;但不出现在基类列表和成员初始化列表。如果模板内某名称依赖于模板参数,则称之为从属名称(dependent name),如果从属名称存在class或类型嵌套,则称为嵌套从属名称(nested dependent name)。因为嵌套从属类型名称(nested dependent type name)存在歧义(即不一定是类型,或许只是静态成员,如 C::iterator
),C++在碰到从属名称时假设它不是类型,此时需要使用typename
声明该类型名称。
Know how to access names in templatized base classes.
了解如何访问模板父类中的名称。考虑到模板父类可能被特化(全特化或者偏特化),其中的名称也是不确定的,因而在继承模板类时C++不去父类中查找名称(与object-oriented C完全不同)。有如下3种方法声明这些名称(对于不存在的名称,即使声明过也会在具现化时编译错):
this->
指针,让编译器假设未特化的父类被继承。using Base<T>::functoin;
声明式,让编译器假设父类存在这样的名称。Base<T>::function();
调用,假设function
已被继承。这样的明确资格修饰(explicit qualification)会关闭virtual绑定行为。Factor parameter-independent code out of templates.
将模板参数无关代码抽离Templates。当发生代码重复时,通常将共同的代码提取出来作为单独的函数。而模板具现化造成的代码重复却更加隐蔽。
long
和int
。Use member function templates to accept “all compatible types”.
使用成员函数模板来接受所有兼容的类型。对于内置指针,时支持隐式转换的,尤其是子类指针转换为基类指针;而对于智能指针模板,不同的模板参数具现化得到的class之间不会反应其参数类型之间的继承关系。
使用成员函数模板可以生成“可接受所有兼容类型”的函数。例如泛化的拷贝构造函数可以实现隐式转换:
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) // 接受所有类型的copy构造函数
:heldPtr(other.get()){...} // 利用成员初始化,对于原是指针不兼容的转换将产生编译错误
// 进而沿用了原始指针一样的兼容策略
T* get const{ return heldPtr; }
private:
T* heldPtr;
};
Define non-member functions inside templates when type conventions are desired.
当需要类型转换时,将非成员函数定义在模板中。如下代码将通不过编译:
template<typename T>
class Rational{...};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){...}
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2;
在执行operator*
时,oneHalf
很容易得到其类型为Rational<int>
,而2
却使编译器不知如何推导T
到底是什么。因为C++编译器在template实参推导过程中,从不考虑隐式类型转换函数(内置隐式转换是兼容的)。可以声明为friend
,则会在具现化Rational<int>
时具现化operator*
的函数声明。此时可以顺利通过编译:
class Ratinoal{
...
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
};
然而链接发生错误,因为只有声明被具现化了。解决方法是将函数本体合并入声明式内:
class Rational{
...
// 这里friend的作用并非访问控制,而是使该函数及时被具现化。
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){
return ...;
}
};
当然这会使operator*
成为内联函数。如果不希望这样,可以把它作为wrapper,调用外部的某个函数。
Use traits classes for information about types.
使用 traits class 表现类型信息。类型信息在有些函数中会用到,例如advance
(用来将迭代器移动n步)。考虑到效率,对于不同类型的迭代器执行的操作应该是不同的。以下介绍如何在该函数中获得类型信息,以进行不同的操作。
STL有五种迭代器:
istream_iterator
。ostream_iterator
。set
, map
。C++ 提供了为迭代器一组卷标结构(tag struct):
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag: public input_iterator_tag{};
struct bidirectional_iterator_tag: public forward_iterator_tag{};
struct random_access_iterator_tag: public bidirectional_iterator_tag{};
然后在不同的迭代器中将其对应的tag class
通过typedef
统一定义为iterator::iterator_category
。例如:
template<...>
class deque{
public:
class iterator{
public:
typedef random_access_iterator_tag iterator_category;
};
...
};
然后提供traits模板,提供各种迭代器类型的iterator_category
(这样的做法只是为了方便,还有对内置类型的支持):
template<typename IterT>
struct iterator_traits{
typedef typename IterT::iterator_category iterator_category;
};
通过特化模板来支持内置类型:
template<typename IterT>
struct iterator_traits<IterT*>{
typedef random_access_iterator_tag iterator_category;
};
至此,我们已经可以在advance
中通过以下代码来测试类型了:
template<typenmae IterT, typename DistT>
void advance(IterT& iter, DistT d){
if(typeid(typename std::iterator_trates<IterT>::iterator_category)
== typeid(typename std::random_access_iterator_tag))
iter += d; // 如果具现化的Iter不是随机访问迭代器,则会在此产生编译错
}
但是注意到typeid
在编译期就可以得到结果,而if
将工作移到运行期,不仅浪费时间,也造成可执行文件膨胀。可以使用函数重载来让编译器在编译期就选择自动合适的函数,定义一系列的重载函数(或函数模板),如:
template<...>
void doAdvance(IterT& iter, DistT d,
std::random_access_terator_tag){
// do sth
}
然后建立控制函数,来调用上述“劳工函数”并传递traits class提供的类型信息:
template<...>
void advance(IterT& iter, DistT d){
doAdvance(iter,d,
typename std::iterator_traits<IterT>::iterator_category()
);
}
Be aware of template metaprogramming
模板元编程(TMP,template metaprogramming)是编写基于模板的C++程序并执行与编译期的过程。例如计算阶乘:
template<unsigned n>
struct Factorial{
enum {value = n* Factorial<n-1>::value};
};
template<>
struct Factorial<0>{
enum {value = 1};
};
...
cout<<Factorial<20>::value<<endl;
TMP可将工作从运行期移到编译期,实现早期的错误侦测和更高的执行效率。可以用来生成“基于策略选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不合适的代码。
Understand the behavior of the new-handler.
std::set_new_handler
可以设置new
失败时的回调函数,而且它将会被反复调用直到找到足够内存。设计良好的new handler必须做以下事情:
new
抛出异常bad_alloc
或不返回(abort
或exit
)new (std::nothrow) Widget
将会在申请空间失败时不抛出异常,而使返回null
,然而在之后的构造函数中仍然产生异常。即Nothrow new不能彻底抑制异常的抛出。
Understand when it makes sense to replace new and delete.
有很多理由需要自定义new
和delete
,包括改善效率、收集heap使用信息、检测错误使用等。
Adhere to convention when writing new and delete.
自定义new
和delete
的惯例:
operator new
应内含一个无穷循环,尝试分配内存,如果无法满足则调用new-handler。应该可以处理0byte申请,处理太大的错误申请。
operator delete
应在收到null
时不做任何事情,处理太大的错误申请。
Write placement delete if you write placement new.
定义placement new的同时也请定义placement delete。
new
一般接受一个参数size_t
,对于有更多参数的new
成为placement new。当申请内存成功,构造函数抛出异常时,C++会尝试调用与new
同样签名的delete
,如果没有则什么都不做(将导致内存泄漏)。
定义placement new的同时,为了使客户可以继续使用正常版本,可以采用using
声明式。
Pay attention to compiler warnings.
Familiarize yourself with the standard library, including TR1.
Faniliarize yourself with Boost.