您的位置:首页 > 编程语言 > C语言/C++

Effective C++ 读书笔记之implemenations(3)

2008-12-10 18:54 309 查看
Item 30. Understanding the ins and outs of inlining.(彻底了解inlining的里里外外)

Inline函数看起来象函数,比宏好得多,但又不需要额外的开销。之所以这样,是因为编译器的最优化机制通常只针对那些“不含函数调用”的代码,而不会对outline函数调用进行优化。

但是天下没有免费的午餐。inline函数的本质是“将对此函数的每一个调用”都以函数本体替换之。因此,这样可能会增加目标代码的大小。过度使用inline函数可能造成程序体积太大。

inline只是对编译器的一个申请,不是强制命令。因此,它可以隐喻提出,也可以明确提出。

隐喻提出是将函数定义于class里面:
class person
{
public:
...
int age() const {return theAge;} //implicit inline function
...
private:
int theAge;
};

明确定义的如:
template<typename T>
inline const T& std::max(const T& a, const T& b)
{return a<b?b:a;}

Inlining在C++程序中是编译期行为,即在编译过程中进行“替换”的。大部分编译器拒绝将过于复杂的函数inlining,同时所有对virtual函数的调用也不支持inline,因为virtual函数只有在运行时才知道要调用谁。

综合起来,也就是说一个表面上看似inline的函数是否真的inline,取决于你的建置环境,即编译器。有时候编译器愿意inlining某一函数,但还是为该函数生成一个函数本体,例如程序要取函数的地址。因此,编译器通常不对“通过函数指针而进行的调用”实施inlining。
例如:
inline void f() {...}
void (*pf)() = f; //函数指针pf指向f
...
f(); //正常调用,函数被inline
pf(); //不被inline,因为通过函数指针实现。

构造函数和析构函数往往被认为是inlining的,例如:
class Base
{
public:
...
private:
std::string bm1, bm2;
};

class Derived: public Base
{
public:
Derived() {} //好像是空的,是inlining吗?
...
private:
std::string dm1,dm2, dm3; //derived的成员
};

但实际上,编译器是会产生一些代码,比如某个异常在对象构建的过程中抛出,此时编译器会将已经构造好的一部分销毁,那么这些事件是编译器在构建/析构函数中实现的。可以想像编译器会实现类似以下的代码。
Derived::Derived()
{
Base::Base();

try {dm1.std::string::string();} //try to construct dm1, destroy it if exception.
catch(...)
{
Base::~Base();
throw;
}

try {dm2.std::string::string();} //try to construct dm2, destroy it if exception.
catch(...)
{
Base::~Base();
throw;
}

try {dm3.std::string::string();} //try to construct dm3, destroy it if exception.
catch(...)
{
Base::~Base();
throw;
}
}
当然,上面这段代码不是真实的代码,实际的代码可能更加复杂。

另外,内联函数的一个一好地方是难以升级,如果是普通的函数,当对其进行升级时,只需要修改生成新的DLL即可,而客户端不需要做什么修改。

注意,大多数编译器不支持inline函数的调试(DEBUG)。

总结:将大多数的inlining限制在小型、频繁的被调用的函数上,这使得日后的调试过程和二进制升级更容易。

Item 31. Minimize compilation dependencies between files.(将文件间的编译依赖关系降到最低)
如果C++的实现(Implementations)做了小的修改,而不是接口,那么重新compile只需要很少的时间。相反,如果选择build则需要很多的时间。问题在于C++的类没有把接口和实现区别开来。例如,以下类:
class Person
{
public:
person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDay() const;
std::string address() const;
...
private:
std::string theName; //Implementation detail
Date theBirthDate; //Implementation detail
Address theAddress; //Implementation detail
};

其中不仅包括类的接口,而且包括实现的细节。这里,如果class person无法取得其它类,如string, Date, Address的定义,那么就无法编译。而这些定义通常是在其它头文件中实现的。因此,person必须包含这些头文件:

#include <string>
#include "date.h"
#include "address.h"

但这样一来,person类的定义文件与上述头文件产生一种编译储存关系(Compilation dependency)。如果这些头文件中有一个必须修改,那么每一个包含class person的文件就得重新编译,任何使用person class的文件必须从新编译。这样效率很低。

你也许会奇怪,为什么C++坚持将class的实现细节放在定义中,为什么不可以这样定义Person,即将实现细节分开?
namespace std { //declaration(no correct)
class string;
}
class Date;
class Address;
class Person
{
public:
person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDay() const;
std::string address() const;
...
};
如果可以那么做,person的客户就只需要在person接口被修改时才重新编译。这样做(前置申请)存在两个问题:
(1) string 不是classes.,它是一个typedef,实际上,它不涉及到templates.
(2) 编译器必须在编译期间知道对象的大小。考虑以下:

int main()
{
int x;
Person p(params); //define person
...
}

对于变量x, 任何编译器都知道其大小。但是对于p,则不知道其大小,因而必须询问类的定义。但是如果公仅前置申明,则无法得知其大小。这个问题在其它语言,如smalltalk, java等上不存在,因为在这些语言的编译器只分配足够的空间给一个指针(指向该对象)使用,即:
int main()
{
int x;
Person * p; //A pointer to Person Object
...
}
当然这也是合法的C++代码,因为我们也可能自己实现“将对象实现细节隐藏在一个指针背后”。针对Person,我们可以这样做:把Person分割成两个classes,一个只提供接口,另一个负责实现该接口。如果负责实现的那个Implementation class取名PersonImpl,那么Person定义如下:
#include <string>
#include <memory>
class PersonImpl; //declare of implemetation classes
class Date;
class Address;
class Person
{
public:
person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDay() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; //指向实现物的指针
};
在这里,main class(person)只内含一个指针成员,指向其实现类PersonImpl,这种设计常称为Pimpl idiom(Pointer to implementation). 这种分离实际上就是“接口与实现分离“的实质,也即以"声明的依存性”替换“定义的依存性”,这也正是编译依存最小化的本质。总结起来包括以下几点:
如果使用object references或object pointer可以完成任务,就不要用objects.
如果能够,尽量以class声明替换class定义,例如:

class Date; //Date 的申明。
Date today();
void clearAppointments(Date d); //Date 的定义

provide separate header files for declarations and definitions.
为了这个原则,则必须定义两个头文件,一个用于声明,一个用于定义,当然这两个文件必须保持一致。

象Person这样使用pimpl idiom的类,往往被称为Handle Classes。这样的内将所有的函数转交给相应的实现类(implementation classes)并由后者完成实际工作。例如,下面是Person两个成员函数的实现:
#include "person.h" //Person classes的定义
#include "personImpl.h" //PersonImpl的实现(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的方法是Interface class,即令person作为一个特殊的抽象基类(Abstract base class)。这个类的目的是详细描述继承类的接口,它自身不带成员变量,也没有构造函数,只有一个virtual 析构函数以下一组pure virtual函数,用来描述整个接口。这种接口类类似于jave和.net的Interfaces。但有一点区别,Java和.net中不允许接口类实现成员变量或成员函数,但是C++不禁止这两者。

一个针对上述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 clas的客户必须有办法创建这个类的新对象,通常会调用一个特殊函数,该函数称为类的工作函数(factory function)或virtual 构造函数。这个函数将真正实例化那个继承类(deried class,实际的实现类),并返回指向对象的指针,并且这个函数通常被申明为static:
class Person
{
public:
...
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));
...
std::cout << pp->name()
<< "was born on"
<< pp->birthDate()
<< "and now lives at "
<< pp->address(); //当pp离开作用域,对象会被自动删除。
...

当然,支持Interface class接口的那个具体类必须被定义,并且真正的构造函数必须被调用,这一切都在具体实现类内发生。例如假设有个具体的实现类RealPerson:

class Real Person: public person
{
public:
RealPerson(const std::string & name, const Date& birthday, const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{}

virtual ~RealPerson() {}
std:: string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName; //Implementation detail
Date theBirthDate; //Implementation detail
Address theAddress; //Implementation detail
};

有了RealPerson后,其Person::create就好写了:
std::tr1::shared_ptr<Person> Person::create((const std::string & name, const Date& birthday, const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
Handle classes和interface classes解除了接口和实现间的藕合关系,从而降低了文件间的编译依赖性(compilation dependencies),当然这样会付出一定的代码,即运行期间损失速度,同时要为每个对象超额分配若干内存。

总结:
(1) 支持“编译依存性最小化”的思想:信赖声明,不要依赖定义,基于此的手段是Handle classes和Interface classes。
(2)程序库头文件应该以“完全的且仅有声明"的方式实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: