尽可能延后变量定义式的出现时间
当定义一个变量而其类型带有一个构造函数或析构函数,当程序的控制流(control flow)到达这个变量定义式时,就要承担构造成本,离开作用域时,就要承担析构成本。即使这个变量没有被使用。
例如:
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if(password.length() < MinimumPasswordLength){
throw logic_error("Password is too short");
}
...
return encrypted;
}
加入异常被抛出,encrypted就会付出无意义的构造和析构成本,因此最好延后encrypted的定义式直到需要它:
std::string encryptPassword(const std::string& password)
{
using namespace std;
if(password.length() < MinimumPasswordLength){
throw logic_error("Password is too short");
}
string encrypted;
... //必要动作,将加密后的密码放入encrypted中。
return encrypted;
}
上述代码还具有还进的空间,encrypted随获得初值但是无任何实参作为初值,调用的时default构造函数。效率比在构造时指定初值要差。
可以以password作为encrypted的初值,直接跳过无意义的default构造过程:
std::string encryptedPassword(const std::string& password)
{
... //检查长度
std::string encrypted(password);
encrypt(encrypted);
return encrypted;
}
当变量在循环中时有两种方案,
方案A:定义于循环外
Widget w;
for(int i = 0; i < n; ++i){
w = 取决于i的某个值;
...
}
方案B:定义于循环内
for(int i = 0; i < n; i++){
Widget w(取决于i的某个值)
}
方法A的成本:1个构造函数+1个析构函数+n个赋值操作
方法B的成本:n个构造函数+n个析构函数
当n值很大时,A方案效率较高,否则B方案较好,另外A方案造成名称w的作用域比做法B要大。所以除非(1)知道赋值成本比“构造+析构”成本低,(2)你正在处理高度敏感(performance-sensitive)的部分,否则应该选择方法B。
尽量少做转型动作
旧式转型:
//都是讲expression转为T
(T)expression //C风格
T(expression)//函数风格
c++提供的四种新式转型
const_cast< T>(expression):
通常用来讲对象的常量性转除(cast away the constness),他也是唯一有此能力的C++style转型操作符。
dynamic_cast< T>(expression):
主要用于执行“安全向下转型”(safe downcasting)用于决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
reinterpret_cast< T>(expression):
意图执行低级转型,实际动作或者结果取决于编译器,这也就表示它不可移植,这一类转型在低级代码以外很少见。
static_cast< T>(expression):
用来执行强迫隐式转换(implicit conversions),如将non-const转为const对象,将int转为double,但是它无法将const转为non-const,这个只有const_cast才能办到。
有时我们会写出似是而非的代码;
class Window { // base class
public:
virtual void onResize( ) { ... } //base class implementation
...
};
class specialWindow: public Window { //derived class
public:
virtual void onResize( ){ //derived onResize
static_cast<Window>(*this).onResize();//将*this转型为Window然后调用其onResize,不可行!
...//此处为SpecialWindow专属行为
}
...
};
上述代码的this调用的不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象的基类成分”的暂时副本上的onResize。如果Window::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。如果SpecialWindow::onResize内修改对象,对象真的被改动。
解决上述情况的办法是拿掉转型动作:
class SpecialWindow: public Window{
public:
virtual void onResize( ){
Window::onResize(); //调用Window::onResize作用于*this身上
...
}
...
}
对于dynamic_cast,值得注意的是,dynamic_cast的许多实现版本执行速度相当慢,之所以需要dynamic_cast通常是因为你想在一个你认定的derived class对象上执行derived class操作函数,但是手上却只有一个==“指向base”的pointer或reference==。有两个一般性做法可以避免这个问题:
第一:使用容器并在其中存储指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要,假设先前的Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果,应当这样做:
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for(VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
(*iter)->blink();
另外一种方法让你通过base class接口处理“所有可能的Windows派生类”的方法是在base class内提供virtual函数做你想对各个Window派生类做的事。
class Window{
public:
virtual void blink() { } //缺省实现代码“什么也没做”
...
};
class SpecialWindow: public Window {
public:
virtual void blink() { ... }; //此class内blink做些事
...
};
typedef std::vector<std::tr1::shared_ptr<Window> VPW;
VPW winPtrs; //容器,内含指针,指向所有可能的Window类型
...
for(VPW::iterator iter = winPtrs.begin( ); iter != winPtrs.end(); ++iter)//此处没有dynamic_cast
(*iter)->blink();
绝对需要避免的一件事是所谓的“连串(cascading) dynamic_casts”
class Window { ... };
... //derived class定义在这里
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin( ); iter != winPtrs.end(); ++iter)
{
if(SpecialWindow1 * psw1 = dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
else if(SpecialWindow2 * psw2 = dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
else if(SpecialWindow3 * psw3 = dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
...
}
如果可以,尽量避免转型,特别是注重效率的代码中避免dynamic_casts.如果有设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将其隐藏在某个函数背后,客户随后可以调用该函数,而不需要将转型放进他们自己的代码内
宁可使用C+±style转型,也不要使用旧式转型
避免返回handles指向对象内部成分
假设设计一个矩形:
class Point{
public:
Point(int x, int y);
...
void setX(int newVal)
void setY(int newVal);
...
};
struct RectData{
Point ulhc;
Point lrhc;
};
class Rectangle{
...
private:
std::tr1::shared_ptr<RecrData> pData;
};
客户可能需要计算Rectangle的范围:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerleft() const { return pData->lrhc; }
...
};
上述做法是自我矛盾的,一方面这里两个函数被声明为const成员函数,因为他们只是为了让客户得知Rectangle的坐标点,另一方面两个函数都返回reference指向private内部数据,调用者可以通过这些reference来修改内部数据。例如:
Point coord1(0, 0);
Point coords(100, 100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50); //此时rec变成了从(50, 0)到(100, 100)
这种做法给了我们两个教训:
==第一,成员变量的封装性最多只等于“reference”的函数的访问级别,本例中的ulhc和lrhc虽然被声明为private,但是实际上确实public,因为public函数upperLeft及lowerRight传出了他们的reference。第二,如果const成员函数传出一个reference, 后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用就可以修改那笔数据,这正是bitwise constness的一个附带结果。
上述的后果都由“成员函数返回reference”引起,如果他们返回的是指针或者迭代器,相同的情况还是会发生。
解决方法:在返回类型上加上const即可:
class Recantangle{
public:
...
const Point& upperLeft( ) const { return pData->ulhc; }
const Point& lowerRight( ) const { return pData->lrhc; }
...
}
这种方法也可能导致dangling handles,即这种handles所指的东西不复存在。
避免使用handles指向对象内部。遵守这个条款可以增加封装性,帮助const成员函数的行为像个const,并将发生dangling handles的可能性降到最低。
为“异常安全”而努力是值得的
“异常安全”的两个条件:
- 不泄露任何资源
- 不允许数据败坏
异常安全函数(Exception-safe functions)提供以下的三个保证之一: - 基本承诺: 如果异常被抛出,程序内的任何事物依然保持在有效状态下
- 强烈保证: 如果异常被抛出,程序状态不改变。
- 不抛掷保证: 承诺绝不抛出异常,因为他们总是能够完成他们原先承诺的功能。
“强烈保证“往往能够以copy-and-swap实现出来,但是”强烈保证“并非对所有函数都可实现或者具备现实意义。
函数提供的”异常安全性保证“通常最高只等于其所调用之各个函数的”异常安全保证“中的最弱者
透彻了解inlining的里里外外
inline函数:调用inline函数不用蒙受函数调用所招致的额外开销,且当使用inline函数时,编译器就因此有能力对执行语境相关最优化。
代价:过度使用inline函数可能导致程序体积太大。
inlining在大多数C++程序中是编译器行为。
将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
不要因为function template出现在头文件,就将他们声明为inline。
将文件之间的编译依存关系降至最低
在C++中,类的定义式不只详细叙述了class接口,还包括十足的实现细目,例如:
class Person{
public:
Person(const std::string& name, cosnt Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::string theName; //实现细目
Date theBirthDate; //实现细目
Address theAddress; //实现细目
}
Person定义文件的上方很可能存在这样的东西:
#include <string>
#include "date.h"
#include "address.h"
当这些头文件有任何一个被改变或者这些头文件所以来的头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person类的文件也必须重新编译,这样的连串编译依存关系(cascading compilation dependencies)会造成很多灾难。
解决方案:以“声明的依存性”替换“定义的依存性”,这也是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式相依。
#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; // 指针,指向实现物
}
private中的指针场被称之为pimpl idiom。
- 如果使用object reference或object point能够完成任务,就不要使用object
- 如果可以,尽量以class声明式替换class定义式
- 为声明式和定义式提供不同的头文件
像Person这样使用pimpl idiom的类往往被称之为handle class。如下:
#include "Person.h" //实现Personclass必须引入其class的定义式
#include "PersonImpl.h"//必须引入PersonImpl class的定义式,否则无法调用成员函数,PersonImpl和Person有着完全相同的成员函数,两者接口完全相同。
Person::Person(const std::string& name, const Date& birthday, const Address& addr) : pImpl(new PersonImpl(name, birthday, addr))
{}
std::string Person::name() const
{
return pImpl->name( );
}
另外一种指定Handle class的办法是,使Person class成为一种特殊的抽象类:Interface class。这种类的目的是详细意义描述derived class的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数用来描述整个接口。
一个针对Person而写的Interface class或许看起来像这样:
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
};
Interface class的客户必须有办法为这种class创建新对象,他们通常调用一个特殊函数,此函数扮演“真正将被具现化”的那个derived class的构造函数角色,这样的函数通常被称之为工厂函数或virtual构造函数。
支持interface class的接口的那个具象类(concrete class)必须被定义出来,而且真正的构造函数必须被调用,一起人都在virtual构造函数实现码所在的文件内秘密发生。
支持“编译依存最小化”的一般构想是:相依于声明式,不要相依与定义式。基于此构想的两个手段是handle class和interface class
==程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在,这种做法不论是否涉及template都适用。