+程序中是编译期行为。
大部分编译器拒绝将太过复杂(含有循环、递归等)的函数inlining,而所有的virtual函数都不能inlining,因为virtual意味着“等待,知道运行期才确定调用哪个函数”,而inline意味“执行前先将动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,你就很难责备它们拒绝将函数本体inlining。
有时候编译器inline某个函数的同时,还可能为其生成一个函数本体(比如程序要取某个line函数地址),值得一提的是,编译器通常不对“通过函数指针而进行的调用”实施inling,这就是说line函数的调用可能是被inlined,也可能不被inlined,取决于调用的实施方式。
“将构造函数和析构函数进行inling”是一个很糟糕的想法。看下面这段代码:
?
Derived::Derived()
{ // 空白Derived构造函数的观念性实现
Base::Base(); // 初始化Base成分
try{
dm1.std::string::string();
}catch(...){
Base::~Base();
throw;
}
try{
dm2.std::string::string();
}catch(...){
dm1.std::string::~string();
Base::~Base();
throw;
}
try{
dm3.std::string::string();
}catch(...){
dm2.std::string::~string();
dm1.std::string::~string();
Base::~Base();
throw;
}
} 这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。尽管如此,这已能准确反映Derived的空白构造函数必须提供的行为。
程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。
?
总结:
1)将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2)不要因为function templates出现在头文件,就将它们声明为inline。
?
条款31:将文件间的编译依存关系降至最低
假设你对c++程序的某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。
然后重新建置这个程序,并预计只花数秒就好,当按下“Build”或键入make,会大吃一惊,因为你意识到整个世界都被重新编译和链接了!
问题是在c++并没有把“将接口从实现中分离”做得很好。class 的定义式不只详细叙述了class接口,还包括十足的实现细目:
?
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::string theName; //实现细目
Date theBirthDate; //实现细目
Address theAddress; //实现细目
};
?
这个class Person无法通过编译,Person定义文件的最上方可能存在这样的东西:
#include
#include date.h
#include address.h
这样写显然在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。可能就会导致开头我们提到的使你陷入窘境的情形出现。所以这里我们采取了另外一种实现方式,即将对象实现细则隐藏与一个指针背后。具体这样做:把Person类分割为两个类,一个只提供接口,另一个负责实现该接口。
?
?
#include
#include
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
pImpl; }
?
这里,Person只内含一个指针成员,指向其实现类(PersonImpl)。这个设计常被称为pimpl idiom(pimpl是“pointer to implementation”的缩写)。
这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每件事都源自于这个简单的涉及策略。
1)如果用object reference 或 object pointer可以完成任务,就不要用objects。
2)如果能够,尽量以class声明式替换class定义式。
3)为声明式和定义式提供不同的头文件。
先Person这样使用pimpl idiom的classes,往往被称为Handle classes。
另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class称之为Interface class。这种class只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。一个针对Person而写的Interface class或许看起来像这样:
?
//Person.h
...
using std::string;
class Date;
class Address;
class Person
{
public:
virtual ~Person();
virtual string name()const = 0;
virtual string birthDate()const = 0;
virtual string address()const = 0;
...
static std::tr1::shared_ptr
create(const string& name,const Date& birthday,const Address& addr);
};
...
// person.cpp
...
class RealPerson:public Person
{
public:
RealPerson(const string& name,const Date& birthday,const Address& addr);
virtual ~RealPerson(){}
string name()const;
...
private:
string name_