二十六:Postpone variable definitions as long as possible. 尽可能延后变量定义式的出现时间。
定义一个变量而其类型带有构造或者析构函数,那么程序的控制流到达这个变量定义式,就会有构造成本。
所以延迟定义,在需要的时候去定义它。
例子:
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;
}
如果中途throw了,encrypted就没有用到,所以可以延迟string的定义。
对于关于循环的操作
Widget w;
for (int i = 0; i < n; ++i)
{
w = ...;
}
for (int i = 0; i < n; ++i)
{
Widget w(...);
}
对于第一种,一个构造 一个析构 n个赋值操作
对于第二种,n个构造,n个析构。
如果一个赋值操作成本低于一组构造+析构成本,a大体好些,尤其是n很大的时候,否则就是b好。
除非1.赋值成本比构造+析构成本低2.正在处理代码中效率高度敏感的部分,否则应该使用做法B。
总结:
尽可能延后变量定义式的出现。这样可增加程序的清晰度并改善程序效率。
二十七:Minimize casting.尽量少做转型动作。
c风格的转型动作:
(T)expression
函数风格的转型动作:
T(expression)
c++的四种转型:
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
例子:
class Window
{
public:
virtual void OnResize() {}
};
class SpecialWindow :public Window {
public:
virtual void OnResize()
{
static_cast<Window>(*this).OnResize();//这边有问题
}
};
这边并非在当前对象上调用Window::onResize,而是在当前对象的base class成分的副本上调用Window::Onresize。
如果Window::OnResize修改了对象的内容,当前对象没有被改动,改动的是副本。如果SpecialWindow::OnResize内如果也修改对象,当前对象会改动。所以会出现baseclass更改没有落实而derivedclass成分的更改落实了。
可以用
Window::OnResize();代替上面的转型操作。
尽量避免dynamic_cast 消耗可能会更大
试着不要这样做:
class Window {};
class SpecialWindow : public Window {
public:
void blink();
};
typedef std::vector<std::tr1::shared_ptr<Window> > VPM;
VPM winPtrs;
for (VPM::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get()))
{
psw->blink();
}
}
改为:
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSM;
VPSM winPtrs;
for (VPSM::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();
}
注:尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进自己的代码里。
宁可使用新式转型。
二十八:Avoid returning "handles" to object internals。避免返回handles指向对象内部成分
refrences, 指针和迭代器统统是所谓的handles(用来获取某个对象)。而返回一个“代表对象内部数据”的handle,随之而来
的便是“降低对象封装性”的风险。
不应该令成员函数返回一个指针指向“访问级别较低”的成员函数,如果这么做,后者的实际访问级别就会提高如同前者,如果要这么做,注意const的使用。
指向内部数据还可能带来空数据的问题(悬吊指针等等)。需要注意判空。
注:
避免返回handles指向对象内部。遵循这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。
二十九:Strive for exception-safe code. 为“异常安全”而努力是值得的。
例子:
class PrettyMenu
{
public:
void ChangeBackground(std::istream imgSrc); //改变背景图像
private:
Mutex mutex;//互斥器
Image* bgImage;//目前的背景图像
int imageChanges;//背景图像被改变的次数。
};
//实现一:
void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
lock(&mutex);//取互斥器
delete bgImage;//
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);//释放互斥器
}
这个实现在异常安全性上很糟糕。
“异常安全”有两个条件:
1.不泄露任何资源。如果上面的new Image(ImgSrc)导致异常,对unlock的调用绝不会执行,于是互斥器永远被把持住了。
2.不允许数据败坏。
确保锁被释放:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
关于数据破坏问题:
class PrettyMenu
{
std::tr1::shared_ptr<Image> bgImage;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
通过只能指针管理资源,并且将计数放在资源设置之后。
一种新的策略:
copy and swap :为你打算修改的对象做出一份副本,然后再副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未修改状态。待所有改变都成功后,再将修改后的那个副本和原对象在一个不跑出异常的操作中置换。
struct PMImp1 {
std::tr1::shared_ptr<Image> bgImage;
int imgaeChanges;
};
class PrettyMenu {
private:
Mutex mutex;
std::tr1::shared_ptr<PMImp1> pImp1;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex);
std::tr1::shared_ptr<PMImp1> pNew(new PMImp1(*pImp1));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImp1, PNew);
}
三十:Understand the ins and outs of inlining透彻了解inlining的里里外外
inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内。
class Person
{
public:
int age() const { return theAge; }//一个隐喻的inline申请:age被定义在class定义式内
private:
int theAge;
};
inlining在大多数c++程序中是编译期行为,少部分是运行期行为,所以Inline函数通常一定被置于头文件内。Templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。
Template的具现化于inlining无关,如果所有根据此template具现化出来的函数都应该inlined,可以将此template声明为inline,
否则就不要声明为inline。
对virtual函数调用会使inlining落空,因为virtual意味着运行期间才知道调用哪个。
编译器通常不对“通过函数指针而进行的调用”实施inlining,所以调用方式也会影响到inline。
inline void f() {}
void(*pf)() = f;
f();//这个调用将被inlined,因为它是一个正常调用
pf();//这个调用或许不被inlined,因为它通过函数指针达成。
inline函数无法随着程序库的升级而升级,如果inline函数修改了,所有用到f的客户端程序必须重新编译。
注:
将大多数inlining限制在小型,被频繁调用的函数上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
不要只因为function tempaltes出现在头文件,就将它们声明为inline。
三十一:Minimize compilation dependencies between files. 将文件间的编译依存关系降至最低。
#include
如果头文件中有任何一个改变,或这些头文件所依赖的其他头文件有任何改变,相关的文件都会重新编译。
考虑一种设计:
#include <string>
#include <memory>
class PersonImp1;
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<PersonImp1> pImp1; //指针, 指向实现物。 std::tr1::shared_ptr
};
这里主类 Person内只含有一个指针成员指向其实现类。这种设计一般被称为 “pointer to implementation”.
这样Person的客户就完全与Dates, Addresses以及Persons的实现细目分离了,那些classes的任何实现修改都不需要Person客户端重新编译。此外由于客户无法看到Person的实现细目,也就不可能写出什么“取决于那些细目”代码,实现接口与实现分离。
(貌似 UE4 的umg实现是这种结构,umg类封装slate类的指针。)
注:
如果使用objects references 或 object pointers可以完成任务,就不要使用objects。因为后者需要定义式。
如果能够,尽量以class声明式替换class定义式。
为声明式和定义式提供不同的头文件。
例如标准程序库头文件的<iosfwd>。 <iosfwd>内含iostream各组件的声明式,其对应定义分布在若干不同的头文件内,包括
<sstream>, <streambuf>, <fstream>和<iostream>
最后:
#include "Person.h"
#include "PersonImp1.h"
Person::Person(const std::string& name, const Data& birthday, const Address& addr)
:pImp1(new PersonImp1(name, birthday, addr))
{
}
std::string Person::name() const {
return pImp1->name();
}
像Person这样使用pimpl idiom的classes, 往往被称为Handle classes.
另一种制作Handle class的办法是,令Person成为一种特殊的abstract base class,称为 Interface class。
一个针对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;
};
static std::tr1::shared_ptr<Person>
create(const std::string& name, const Date& birthday, const Address& addr);
提供一个工厂函数。
std::string name;
Date dateOfBirth;
Address address;
//...
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));