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

 假设你对C++程序的某个class实现文件做了些轻微修改,假如只改了private部分,然后,重新建置这个程序,你以为只需花数秒,但你会发现,整个世界都被重新编译和连接了。

上述问题出现在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 assress() const;
    //...
private:
    std::string theName;//实现细目
    Date theBirthDate;//实现细目
    Address theAddress;//实现细目
};

若编译器没有取得class Person实现代码所用到的class string,Date和Address的定义式,这里的Person就无法通过编译。

这样的定义式通常由#include指示符提供,所以Person定义文件的最上方很可能存在这样的东西:

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

但,不幸的是,这样一来在Person定义文件和其含入文件之间就形成了一种编译依存关系(compilation dependency)。

若这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。这样的连串编译依存关系(cascading compilation dependencies)会对许多项目造成难以形容的灾难。

或许你会觉得奇怪,为什么C++坚持将class的实现细目置于class定义式中?为什么不这样定义Person,将实现细目分开叙述?

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 assress() const;
    //...
};

若可以这么做,Person的客户就只需要在Person接口被修改过时才重新编译。

这个想法存在两个问题:

1.string不是个class,他是个typedef(定义为basic_string<chat>)。因此,上述针对string而做的前置声明并不正确;正确的前置声明比较复杂,因为涉及额外的template。然而这不要紧,因为你本来就不该尝试手工声明一部分标准程序库。你应该仅仅使用适当的#include完成目的。

2.关于”前置声明每一件东西“的第二个困难是,编译器必须在编译期间知道对象的大小。

例如:

int main()
{
    int x;//定义一个int
    Person p(params);//定义一个Person
    //...
}

当编译器看到x的定义式,它知道必须分配多少内存(通常位于stack内)才够有一个int。这没有问题,因为每一个编译器都知道一个int有多大。

当编译器看到p的定义式,它也知道必须分配足够空间以放置一个Person,但它如何知道一个Person对象有多大呢?编译器获得这项信息的唯一办法是询问class定义式。然而,若class定义式可以合法地不列出实现细目,编译器如何知道该分配多少空间?

我们的一个解决方法是:把Person分割为两个class,一个只提供接口,另一个负责实现该接口。若负责实现的那个所谓implementation class(实现类)取名为PersonImpl,则Person的定义如下:

#include <string>//标准程序库组件不该被前置声明
#include <memory>//此乃为了tr1::shared_ptr而含入
class PersonImpl;//Person实现类的前置声明
class Date;//Person接口用到的class的前置声明
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 assress() const;
    //...
private:
    std::tr1::shared_ptr<PersonImpl> pImpl;//指针,指向实现物
};

上述代码中,main class(Person)只内含一个指针成员(这里使用tr1::shared_ptr)指向其实现类(PersonImpl)。这般设计常被称为pimpl idiom(pimpl是"pointer to implementation"的缩写)。这种class内的指针名称往往就是pImpl,就像上述代码那样。

这样设计,Person的客户就完全与Date,Adress以及Person的实现细目分离了。那些class的任何实现修改都不需要Person客户端重新编译。此外,由于客户无法看到Person的实现细目,也就不可能写出什么”取决于那些细目“的代码。

这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策略:

 1.若使用object reference或object pointer可以完成任务,就不要使用object。

你可以只靠一个类型声明式就定义出指向该类型的reference和pointer;但若定义某类型的object,就需要用到该类型的定义式。

2.如果可以,尽量以class声明式替换class定义式。

注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以by value方式传递该类型的参数(或返回值)亦然:

class Date;//class声明式
Date today();//没问题,这里不需要Date的定义式
void clearAppointments(Date d);

 声明roday函数和clearAppointments函数而无需定义Date,这种能力可能会令你惊讶,但它并不是真的那么神奇。一旦任何人调用那些函数,调用之前Date定义式一定得先曝光才行。

那么或许你会疑惑,何必费心声明一个没人调用的函数呢?注意,并非没人使用,而是并非每个人都调用。

假设你有一个函数库内含数百个函数声明,不太可能每个用户叫便每一个函数。若能够将“提供class定义式”(通过#include完成)的义务从“函数声明所在”的头文件转移到“内含函数调用”的客户文件,便可将“并非真正必要的类型定义”与客户端之间的编译依存性去除掉。

3.为声明式和定义式提供不同的头文件

为了严守上述准则,需要两个头文件,  一个用于声明式,一个用于定义式。当然,这些文件必须保持一致性,若有个声明式被改变了,两个文件都得改变。因此程序库客户应该总是#include一个声明文件而非前置声明若干函数,程序库作者也应该提供这两个头文件。

例如,Date的客户若希望声明today和clearAppointments,他们不该像先前那样以手工方式前置声明Date,而是应该#include适当的、内含声明式的头文件:

#include "datefwd.h"//这个头文件内声明(但未定义)class Date
Date today();//同前
void clearAppointments(Date d);

像Person这样使用pimpl idiom的class,往往被称为Handle class。也许你会纳闷,这样的class如何真正做点事情。

办法之一是将它们的所有函数转交给相应的实现类(implementation class)并由后者完成实际工作。

例如,下面是Person的两个成员函数的实现:

#include "Person.h"
#include "PersonImpl.h"
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构造函数以new调用PersonImpl构造函数,以及Person::name函数内调用PersonImpl::name。这是重要的,让Person变成一个Handle class并不会改变它做的事,只会改变它做事的方法。

另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class(抽象基类),称为interface class。这种class的目的是详细一一描述derived class的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。

一个针对Person而写的interface class或许看起来像这样:

class Person {
public:
    virtual ~Person();
    virtual std::string name() const=0;
    virtual std::string brithDate() const = 0;
    virtual std::string address() const = 0;
    //...
};

这个class的客户必须以Person的pointer和reference来撰写应用程序,因为它不可能针对“内含pure virtual函数”的Person class具现出实体。就像Handle class的客户一样,除非Interface class的接口被修改否则其客户不需重新编译。

Interface class的客户必须有办法为这种class创建新对象。他们通常调用一个特殊函数,此函数扮演“真正将被具现化”的那个derived class的构造函数角色。这样的函数通常称为factory(工厂)函数或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);
    //返回一个tr1::shared_ptr,指向一个新的Person,并以给定的参数初始化
    //...
};

客户会这样使用它们:

std::string name;
Date dateOfBirth;
Address address;
//...
//创建一个对象,支持Person接口
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
//...
//通过Person的接口使用这个对象
cout << pp->name() << "  was born on " 
     << pp->birthDate() << " and now lives at " 
     << pp->address();
//...
//当pp离开作用域,对象会被自动删除

当然,支持Interface class接口的那个具象类(concrete class)必须被定义出来,而且真正的构造函数必须被调用。一切都在virtual构造函数实现码所在的文件内秘密发生。假设Interface class Person有个具象的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(Person)继承接口规格,然后实现出接口所覆盖的函数。第二个实现法涉及多重继承。

Handle class和Interface class解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。

但这是有代价的,它使你在运行期丧失若干速度,又让你为每个对象超额付出若干内存。

在Handle class身上,成员函数必须通过implementation pointer取得对象数据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加implementation pointer的大小。最后,implementation pointer必须初始化(在Handle class构造函数内),指向一个动态分配得来的implementation object,所以你将蒙受因动态内存分配(及其后的释放动作)而来的额外开销,以及遭遇bad_alloc异常(内存不足)的可能性。

至于Interface class,由于每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃(indirect jump)成本。此外Interface class派生的对象必须内含一个vptr(virtual table pointer),这个指针可能会增加存放对象所需的内存数量——实际取决于这个对象除了Interface class之外是否还有其他virtual函数来源。

最后,不论Handle class或Interface class,一旦脱离inline函数都无法有太大作为。

总结

1.支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle class和Interface class。

2.程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及template都适用。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/25101.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【WebRTC】音视频通信

WebRTC对等体还需要查找并交换本地和远程音频和视频媒体信息&#xff0c;例如分辨率和编解码器功能。 交换媒体配置信息的信令通过使用被称为SDP的会话描述协议格式来交换&#xff0c;被称为提议和应答的元数据块 WebRTC 音视频通信基本流程 一方发起调用 getUserMedia 打开本…

Go网络通信

Go中HTTP协议客户端实现 Go语言标准库内置了net/http包&#xff0c;涵盖了HTTP客户端和服务端具体的实现方式。内置的net/http包提供了最简洁的HTTP客户端实现方式&#xff0c;无须借助第三方网络通信库&#xff0c;就可以直接使用HTTP中用得最多的GET和POST方式请求数据。 fun…

【Python】内置函数

文章目录 反射相关【4】基础数据类型相关【38】和数字相关&#xff08;14&#xff09;数据类型 <4>bool([x])int((x, base10)float([x])complex([real[, imag]]) 进制转换 <3>bin(x)oct(x)hex(x) 数学运算&#xff08;7&#xff09;abs(x)divmod(a, b)round(x [, n…

vue 阻止事件冒泡常用的方法

在 Vue 中&#xff0c;阻止事件冒泡有两种常用方法&#xff1a; 1. 使用 event.stopPropagation() 方法&#xff1a; 在事件处理函数中&#xff0c;可以通过调用事件对象的 stopPropagation() 方法来阻止事件冒泡。例如&#xff1a; html <template> <div click"…

00后太卷了,搞的我们这些老油条太难受了......

前几天我们公司一下子也来了几个新人&#xff0c;这些年前人是真能熬啊&#xff0c;本来我们几个老油子都是每天稍微加会班就打算走了&#xff0c;这几个新人一直不走&#xff0c;搞得我们也不好走。 2023年春招结束了&#xff0c;最近内卷严重&#xff0c;各种跳槽裁员&#x…

报表控件FastReport使用指南——使用NuGet包创建PDF文档

FastReport 是功能齐全的报表控件&#xff0c;可以帮助开发者可以快速并高效地为.NET&#xff0c;VCL&#xff0c;COM&#xff0c;ActiveX应用程序添加报表支持&#xff0c;由于其独特的编程原则&#xff0c;现在已经成为了Delphi平台最优秀的报表控件&#xff0c;支持将编程开…

多层网关已成过去,网关多合一成潮流,网关改造正当时丨Higress 正式发布 1.0 版本

作者&#xff1a;Higress 团队 01 前言 K8s 通过 Ingress / Gateway API 将网关标准化&#xff0c;逐步将安全网关、流量网关、微服务网关内聚&#xff0c;解决从单体到微服务到云原生多层网关的复杂度&#xff0c;合久必分&#xff0c;分久必合&#xff0c;多层网关已成过去…

(3)NUC980 kenerl编译

解压 用到的配置文件位置&#xff1a; /NUC980-linux-4.4.y-master/arch/arm/configs/nuc980_defconfig 执行&#xff1a; 编译linux内核源码。了解其 配置文件在 arch/arm/configs/nuc980_defconfig (1) make nuc980_defconfig 载入配置文件 (2) make menuconfig --->Devi…

SAP MM 根据采购订单反查采购申请

如何通过采购订单号查询到其前端的采购申请号。 首先从采购申请的相关报表着手&#xff0c;比如ME5A, 发现它是可以满足需求的。 例如&#xff1a;如下的采购订单&#xff0c; 该订单是由采购申请10003364转过来的。 如果想通过这个采购订单找到对应的采购申请&#xff0c;在…

Python3中goto的用法

Python3代码指定跳转可以使用goto这个库&#xff1a; 安装&#xff1a; pip install goto-statement 一般安装的版本是1.2 需要做以下修改才能正常使用&#xff1a; python 使用goto&#xff0c;遇到的问题解决_奶嘴偷走初吻的博客-CSDN博客python goto 出现报错:Attribut…

情绪管理ABC法

情绪管理ABC法 是由著名心理学家艾利斯&#xff08;Albert Ellis&#xff09;提出的一种情绪管理方法。 模型介绍 情绪&#xff0c;不取决于发生的事实&#xff0c;取决于我们如何看待这件事ABC理论认为&#xff0c;我们的情绪©&#xff0c;其实与发生的事件(A)无关&…

类的成员之:构造器(构造方法)

1.构造器的特征&#xff1a; 它具有与类相同的名称它不声明返回值类型。&#xff08;与声明为void不同&#xff09;不能被static、final、synchronized、abstract、native修饰&#xff0c;不能有return语句返回值 2.构造器的作用&#xff1a; 1.创建对象2.初始化对象的…

PID算法在流量控制中的应用

目录 增量式或位置式 目录 增量式或位置式 PID控制周期 T1 时间 T2 约4ms PID C代码 最近有小伙伴向我提问关于PID的问题&#xff1a;通过比例阀控制水流速度&#xff08;流量&#xff09;&#xff0c; 使用增量式还是位置式 PID&#xff1f;他的比例法驱动频率是500Hz…

linux环境搭建

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:介绍学习如何使用云服务器搭建Linux的环境. 前言 linux介绍…

【计算机网络中ip概念总结】【平时我们说的ip 到底是什么】【计算机网络中 ip地址是什么】

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

Linux系统之编译安装python3

Linux系统之编译安装python3 一、python3介绍1. python3简介2. python3特点 二、检查本地环境1. 检查本地操作系统版本2. 检查内核版本3. 检查当前python版本 三、安装前准备工作四、下载python最新版本源码包1. 访问python官网2. 创建下载目录3. 下载python源码包4. 解压pytho…

Redis 常见面试题

1. 认识Redis Redis是一个开源的内存数据结构存储&#xff0c;Redis是一个基于内存的数据库&#xff0c;对数据的读写都在内存中完成&#xff0c;因此数据读写速度非常快&#xff0c;常用于缓存&#xff0c;分布式锁等&#xff0c;MySQL的表数据都存储在 t_order.ibd&#xff…

国内可以免费使用的GPT

一、wetab新标签页 教程&#xff1a;https://diwlwltzssn.feishu.cn/docx/MnHhdvxATomBnMxfas2cm8wWnVd 装GPT界面&#xff1a;https://microsoftedge.microsoft.com/addons/detail/wetab%E5%85%8D%E8%B4%B9chatgpt%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5/bpelnogcookhocnaokfp…

华为OD机试真题(Java),跳跃游戏 II(100%通过+复盘思路)

一、题目描述 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: 0 <= j <= nums[i]0i + j <返回到达 nums[n - 1] 的最小跳跃…

C++学习笔记(四): 类、头文件、对象

一个类定义了一个类型&#xff0c;以及与其关联的一组操作。所谓类&#xff0c;是用户自定义的数据类型。 类机制是C最重要的特性之一。实际上&#xff0c;C最初的一个设计焦点就是能定义使用上像内置类型一样自然的类类型&#xff08;class type&#xff09;。 类的定义一般分…
最新文章