AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

C++箴言:最小化文件之间的编译依赖

51自学网 http://www.wanshiok.com

  像 Person 这样的使用 pimpl 惯用法的类经常被称为 Handle 类。为了避免你对这样的类实际上做什么事的好奇心,一种方法是将所有对他们的函数调用都转送给相应的实现类,而使用实现类来做真正的工作。例如,这就是两个 Person 的成员函数可以被如何实现的例子:

#include "Person.h" // we’re implementing the Person class,
// so we must #include its class definition

#include "PersonImpl.h" // we must also #include PersonImpl’s class
// definition, otherwise we couldn’t call
// its member functions; note that
// PersonImpl has exactly the same
// member functions as Person - their
// interfaces are identical

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();
}

  注意 Person 的成员函数是如何调用 PersonImpl 的成员函数的,以及 Person::name 是如何调用 PersonImpl::name 的。这很重要。使 Person 成为一个 Handle 类不需要改变 Person 要做的事情,仅仅是改变了它做事的方法。

  另一个不同于 Handle 类的候选方法是使 Person 成为一个被叫做 Interface 类的特殊种类的抽象基类。这样一个类的作用是为派生类指定一个接口。结果,它的典型特征是没有数据成员,没有构造函数,有一个虚析构函数和一组指定接口的纯虚函数。

  Interface 类类似 Java 和 .NET 中的 Interfaces,但是 C++ 并不会为 Interface 类强加那些 Java 和 .NET 为 Interfaces 强加的种种约束。例如,Java 和 .NET 都不允许 Interfaces 中有数据成员和函数实现,但是 C++ 不禁止这些事情。C++ 的巨大弹性是有用处的。在一个继承体系的所有类中非虚拟函数的实现应该相同,因此将这样的函数实现为声明它们的 Interface 类的一部分就是有意义的。

  一个 Person 的 Interface 类可能就像这样:

class Person {
public:
 virtual ~Person();

 virtual std::string name() const = 0;
 virtual std::string birthDate() const = 0;
 virtual std::string address() const = 0;
 ...
};

  这个类的客户必须针对 Person 的指针或引用编程,因为实例化包含纯虚函数的类是不可能的。(然而,实例化从 Person 派生的类是可能的)和 Handle 类的客户一样,除非 Interface 类的接口发生变化,否则 Interface 类的客户不需要重新编译。

  一个 Interface 类的客户必须有办法创建新的对象。他们一般通过调用一个为“可以真正实例化的派生类”扮演构造函数的角色的函数做到这一点的。这样的函数一般称为 factory 函数或虚拟构造函数(virtual constructors)。他们返回指向动态分配的支持 Interface 类的接口的对象的指针(智能指针更合适)。这样的函数在 Interface 类内部一般声明为 static:

class Person {
public:
 ...

 static std::tr1::shared_ptr<Person> // return a tr1::shared_ptr to a new
 create(const std::string& name, // Person initialized with the
 const Date& birthday, // given params; see Item 18 for
 const Address& addr); // why a tr1::shared_ptr is returned
 ...
};

  客户就像这样使用它们:

std::string name;
Date dateOfBirth;
Address address;
...

// create an object supporting the Person interface
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));

...

std::cout << pp->name() // use the object via the
<< " was born on " // Person interface
<< pp->birthDate()
<< " and now lives at "
<< pp->address();
... // the object is automatically
// deleted when pp goes out of

  当然,在某些地点,必须定义支持 Interface 类的接口的具体类并调用真正的构造函数。这所有的一切发生的场合,在那个文件中所包含虚拟构造函数的实现之后的地方。例如,Interface 类 Person 可以有一个提供了它继承到的虚函数的实现的具体的派生类 RealPerson:

class RealPerson: 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; // implementations of these
 std::string birthDate() const; // functions are not shown, but
 std::string address() const; // they are easy to imagine

private:
 std::string theName;
 Date theBirthDate;
 Address theAddress;
};

  对这个特定的 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));
}

  Person::create 的一个更现实的实现会创建不同派生类型的对象,依赖于诸如,其他函数的参数值,从文件或数据库读出的数据,环境变量等等。

  RealPerson 示范了两个最通用的实现一个 Interface 类机制之一:从 Interface 类(Person)继承它的接口规格,然后实现接口中的函数。实现一个 Interface 类的第二个方法包含多继承(multiple inheritance),在 Item 40 中探讨这个话题。

  Handle 类和 Interface 类从实现中分离出接口,因此减少了文件之间的编译依赖。如果你是一个喜好挖苦的人,我知道你正在找小号字体写成的限制。“所有这些把戏会骗走我什么呢?”你小声嘀咕着。答案是计算机科学中非常平常的:它会消耗一些运行时的速度,再加上每个对象的一些额外的内存。

  在 Handle 类的情况下,成员函数必须通过实现的指针得到对象的数据。这就在每次访问中增加了一个间接层。而且你必须在存储每一个对象所需的内存量中增加这一实现的指针的大小。最后,这一实现的指针必须被初始化(在 Handle 类的构造函数中)为指向一个动态分配的实现的对象,所以你要承受动态内存分配(以及随后的释放)所固有的成本和遭遇 bad_alloc (out-of-memory) 异常的可能性。

  对于 Interface 类,每一个函数调用都是虚拟的,所以你每调用一次函数就要支付一个间接跳转的成本。还有,从 Interface 派生的对象必须包含一个 virtual table 指针。这个指针可能增加存储一个对象所需的内存的量,依赖于这个 Interface 类是否是这个对象的虚函数的唯一来源。

  最后,无论 Handle 类还是 Interface 类都不能在 inline 函数的外面大量使用。函数本体一般必须在头文件中才能做到 inline,但是 Handle 类和 Interface 类一般都设计成隐藏类似函数本体这样的实现细节。

  然而,因为它们所涉及到的成本而简单地放弃 Handle 类和 Interface 类会成为一个严重的错误。虚拟函数也是一样,但你还是不能放弃它们,你能吗?(如果能,你看错书了。)作为替代,考虑以一种改进的方式使用这些技术。在开发过程中,使用 Handle 类和 Interface 类来最小化实现发生变化时对客户的影响。 当能看出在速度和/或大小上的不同足以证明增加类之间的耦合是值得的时候,可以用具体类取代 Handle 类和 Interface 类供产品使用。

  Things to Remember

  最小化编译依赖后面的一般想法是用对声明的依赖取代对定义的依赖。基于此想法的两个方法是 Handle 类和 Interface 类。

  库头文件应该以完整并且只有声明的形式存在。无论是否包含模板都适用于这一点。

 
 

上一篇:C++箴言:理解inline化的介入和排除  下一篇:C/C++编程新手错误语录