条款31:将文件间的编译依存关系降至最低

1、文件间依存度高的话带来的影响?

假如你修改了C++ class实现文件,修改的仅仅是实现,而没有修改接口,而且只修改private部分。此时,重新构建这个程序时,会发现整个文件、以及用到该class 的文件都被会被重新编译和连接,这不是我们想要看到的。

2、出现上述问题的原因

问题出在C++没有把关于接口与实现相分离这件事做好。C++ 的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 string,Date和Address的定义式时,它无法通过编译它所需要的这样的定义式往往由#include <>提供(里面有class string,Date和Address的实现代码)。例如本例中需要:。

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

如果这些头文件中(或头文件所依赖的头文件)的一个的实现被改变了,那么每一个用到class 类的文件都得重新编译。这就是所谓的文件间的依存度比较高。

3、解决文件间依存性的一个不成熟方案

C++ 为什么不如下述这样做,以实现接口与实现分离呢?

namespace std { 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 birthDate() const; 
    std::string address() const; 
    ... 
};

上述设想不成立,原因有两条:

  • 第一:string并不是一个class,他是一个typedef(定义为basic_string)。因此上述针对string做的声明并不正确;正确的声明比较复杂,因为涉及额外的tempalte。退一步讲,你本来就不应该尝试手工声明标准库程序的一部分,你应该仅仅使用适当的#include完成目的。其实标准头文件这也不是编译的瓶颈,也有解决的方法。例如:你可以值改变你的接口涉及,避免使用
    标准头文件的非法的#include。
  • 第二:编译器必须在编译期间知道对象的大小。这才是问题的关键。例如:下述程序中,当编译器看到x时,由于知道它是int类型的,也就知道需要为它分配多大的空间。但是当编译器看到自定义的类Person对象p时,编译器必须看到Person的类定义才能知道为p对象分配多大的内存。如果class中没有实现细目,那么编译器就无法确定为其分配多大内存。

4、解决文件间依存关系的正确手法一:handle class

(1)基本思想

将对象的实现细目隐藏到一个指针(通常是一个智能指针)背后。
例如:

#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; // 指向实现物的指针 
};

上述程序中,将原本的Person 类写成两个部分,接口那部分是主要的部分,其中含了一个智能指针,指向实现细目。而实现细目另外定义了一个类:PersonImpl。这种设计手法被称为:pimpl idiom。
注意:pimpl 指的是 pointer to implementation。这种class内的指针往往被称为:pImpl指针。上述class的写法 往往被称为handle class。

(2)上述手法的好处

实现了接口与实现的分离。即:Person的客户与 Date、Address、以及Person的实现细目就分离了。
实现接口与实现的分离所带来的的好处:

  • 这些class的修改,都不需要Person客户进行重新编译。
  • 而且由于客户无法看到实现细目,也就不能写出由这些实现细目所决定的代码。
(3)上述实现的关键
(4)该手法下的其他情况细分
  • 如果用 object reference 或 object pointer 可以完成任务,就不要用 objects。
    可以只靠声明式定义出指向该类型的 pointer 和 reference;但如果定义某类型的 objects,就需要用到该类型的定义式。
  • 如果能够,尽量以 class 声明式替换 class 定义式。
    当你声明一个函数而它用到某个 class 时,你并不需要该 class 的定义式,纵使函数以 by value 方式传递该类型的参数(或返回值)亦然。
  • 为声明式和定义式提供不同的头文件。
    两个头文件应该包吃一致性,其中一个头文件发生改变,另一个就得也改变。一个内含了class 接口的定义,另一个仅仅内含声明。

例如:只含声明式的Date class 的头文件应该命名为datefwd.h(有一定的命名规则)。
注意:本条款适用于template 也适用于 non-template。

5、解决文件间依存关系的正确手法二:另外一种实现handle class的手法,interface class手法

(1)基本思想

令Person class 成为一种特殊的abstract base class (抽象基类),称为interface class。这样的类通常:没有成员变量,也没有构造函数,只有一个virtual 的析构函数以及一组pure virtual 用来描述接口。

(2)C++ 接口类与其他语言接口类的不同

像.net和java 的接口,他们不允许在接口类中定义成员函数和成员变量。但是C++的接口类并不禁止,这样的规则使得C++语言具有更大的弹性。

(3)interface class 类创建对象的方式

由于这样的类往往没有构造函数,因此通过工厂函数或者virtual构造函数创建,他们返回指针,指向动态分配对象所得的对象,这样的对象支持interface class的 接口,这样的函数在interface 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 dateBirth; 
Address address; 
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address)); 
... 
std::cout << pp->name() 
            << "was born on " 
            << PP->birthDate() 
            << " and now lives at " 
            << pp->address(); 
...

当然支持 interface class 接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。
假设有个 derived class RealPerson,提供继承而来的 virtual 函数的实现:

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; 
    std::string birthDate() const; 
    std::string address() const; 

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 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、独自文件或数据库的数据、环境变量等等。

RealPerson 示范实现了 Interface class 的两个最常见机制之一:从 interface class 继承接口规格,然后实现出接口所覆盖的函数。

6、两种手法带来的开销

handle classes 和 interface classes 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。

  • handle classe

    成员函数必须通过 implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。每个对象消耗的内存必须增加一个 implementation pointer 的大小。 implementation pointer 必须初始化指向一个动态分配的 implementation object,所以还得蒙受因动态内存分配儿带来的额外开销。

  • Interface classe

    由于每个函数都是 virtual,必须为每次函数调用付出一个间接跳跃。此外 Interface class 派生的对象必须内含一个 vptr(virtual table pointer)。

7、两种手法的适用场景

在程序开发过程中使用 handle class 和 interface class 以求实现码有所改变时对其客户带来最小冲击。

而当他们导致速度和/或大小差异过于重大以至于 class 之间的耦合相形之下不成为关键时,就以具象类(concrete class)替换 handle class 和 interface class。

原文地址:https://www.cnblogs.com/lasnitch/p/12764163.html