怎样削减C++代码间依赖

2023-10-03 20:10
文章标签 代码 c++ 依赖 削减 怎样

本文主要是介绍怎样削减C++代码间依赖,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 介绍

C++程序编译很慢,特别是大型工程,你都可以趁着编译的功夫喝杯咖啡。这里面既有天灾也有人祸。

天灾请看此文,本文探讨怎么避免人祸。

2. 编译过程

后文需要,我们先了解C++源代码的编译过程
这里写图片描述
为叙述方便,图中各文件类型称呼如下:

  • .h 头文件
  • .cc 源文件
  • .o 目标文件

本文不探讨链接过程,只考虑编译和预处理。

预处理

经过预处理后,头文件就消失了。因为 #include 预处理指令将其展开在源文件中,编译阶段是不需要头文件的。常见的预处理指令还有 #if, #elif, #end, #ifndef, #define 等。

#include 其实非常简单,就是把文件在当前位置展开,没有任何多余的功能和限制。所以你不仅能 #include 头文件,还可以 #include .cc文件, 甚至任何文件。

编译

经过预处理后,只剩下源文件了,编译就是把每个源文件转换成目标文件。记住,源文件和目标文件是一一对应的。

编译的实质,是根据源文件生成目标文件的代码段数据段。这一点对于理解前向声明非常重要。

图解说

假设我们有4个源文件和4个头文件,include关系如下

//  A.h                             A.cc
#include "B.h"                     #include "A.h"----------------------------------------------------
//  B.h                             B.cc
#include "C.h"                     #include "B.h"----------------------------------------------------
//  C.h                             C.cc
#include nothing                   #include "C.h"----------------------------------------------------
//  D.h                             D.cc
#include "A.h"                     #include "D.h"#include "C.h" 

我们可以用一张图来表示这8个文件的预处理和编译两个阶段
这里写图片描述
虚箭头表示编译,是一对一的,没什么好说的。实箭头表示 #incude 关系。如果一个文件有修改,那么所有直接或间接依赖它的源文件都要重新编译。下面我们就根据此图讲解C++工程中的代码依赖问题。

3. 重复include

D.cc两次包含C.h,一次直接包含,一次通过间接包含。这会引起重定义错误,有两种方法解决。

经典的 ifndef

// A.h
#ifndef A_H
#define A_H// class definition#endif

或者 pragma once

// A.h
#pragma once// class defnition

4. 循环依赖

一旦代码中有循环依赖,编译将失败。上文的8个文件没有循环依赖,图中找不到有向循环路径。

万一你的代码出现循环依赖,请将导致循环依赖的代码提出来,放到新文件中。

5. 前向声明

前两个问题关乎对错,跟编译速度关系不大。现在开始,我们探讨怎么加快代码编译速度。

我们向文件中加点东西

//  A.h                             A.cc
#include "B.h"                     #include "A.h"class A {                          ....
public:void f1(B* b);                   void A::f1(B* b) {b->f3();void f2();                       }
};----------------------------------------------------
//  B.h                             B.cc
#include "C.h"                     #include "B.h"class B {
public:void f3();
};----------------------------------------------------
//  C.h                             C.cc
#include nothing                   #include "C.h"----------------------------------------------------
//  D.h                             D.cc
#include "A.h"                     #include "D.h"#include "C.h"class D {
public:
private:A a;
};

根据前文的依赖图,如果 B.h 修改,那么 A.cc 和 D.cc 都要重新编译。但是, D.cc 不应该重新编译,因为 class D 只受 class A 内存布局的影响,而 class A 的内存布局与 class B 没有关系,只是f1需要一个class B指针的参数而已。

于是C++允许 A.h 不包含 B.h ,只需声明 class B 即可,告诉编译器 B 这丫是个类名哈。A.h用前向声明替代include,但是 A.cc 中利用了 class B 的实现,所以 A.cc 要加一条 include(放心,源文件中的include不会传染,很少有 include 源文件的)

//  A.h                             A.cc
class B;                           #include "A.h"#include "B.h"class A {                          ....
public:void f1(B* b);                   void A::f1(B* b) {b->f3();void f2();                       }
};

依赖线一增一减,数量虽然没变,但是效果改善不少啊
这里写图片描述

6. Pimpl idiom

是不是有点儿累了,先歇会儿,然后再来看另外一个削减依赖的重磅武器 Piml(pointer to implementation)。

pimpl idiom主要运用在库的接口设计中。如果团队人数达100,有一个维护基础库的小组。原则上除了接口升级,基础库的任何升级都不应该触发应用层模块的重编译。

我们假设 A.h 是基础库的接口,B.cc 是应用层源文件。

// A.h                      A.cc
#include "xx.h"
#include "yy.h"
#include "zz.h"class A {                       
public: A();                     A() { ... }~A();                    ~A() { ... }void f1();               void A::f1() { ... }void f2();               void A::f2() { ... }
private:XX x;YY y;ZZ z;
};-----------------------------------------------------
// B.cc
#include "A.h"class B {
public:
private:A a;
};

依赖图如下
这里写图片描述
从依赖图明显看出这种代码组织方式有缺陷,XX.h, YY.h, ZZ.h 任何一个文件的修改都会触发 B.cc 重编译。那三个文件都是库内部的文件,原则上是与B.cc无关的,并且这三个头文件很可能包含别的头文件,导致程序库最深处的修改都能轻易触发应用层代码重编译。

这时候该Pimpl显身手了,我们把 A.h, A.cc 改造如下

// A.h                         A.cc
class A {                      #include "A.h"   
public:                        #include "XX.h"A();                         #include "YY.h"~A();                        #include "ZZ.h"void f1();                 void f2();                   class A::Impl {
private:                       public:class Impl;                    Impl() { ... }Impl* impl_;                   ~Impl() { ... }
};                               void f1() { ... }void f2() { ... }private:XX x;YY y;ZZ z;};A::A() : impl_(new A::Impl()) {}A::~A() { delete impl_; }void A::f1() {impl_->f1();}void A::f2() {impl_->f2();}

把A中所有非接口的东西都移到Impl中去,依赖图变成下面的样子。
这里写图片描述
从依赖图看出,只有 A.h 的修改才能触发 B.cc 重编译,而 A.h 中只有库接口,库的接口都变了,应用层当然要重编译咯。

7. 参考资料

1.C++ Compilation Speed
2.Pimpl For Compile-Time Encapsulation (Modern C++)
本文转自:怎样削减C++代码间依赖
作者:wankai

这篇关于怎样削减C++代码间依赖的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/652

相关文章

每天认识几个maven依赖(ActiveMQ+activemq-jaxb+activesoap+activespace+adarwin)

八、ActiveMQ 1、是什么? ActiveMQ 是一个开源的消息中间件(Message Broker),由 Apache 软件基金会开发和维护。它实现了 Java 消息服务(Java Message Service, JMS)规范,并支持多种消息传递协议,包括 AMQP、MQTT 和 OpenWire 等。 2、有什么用? 可靠性:ActiveMQ 提供了消息持久性和事务支持,确保消

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能