当前位置: 首页 > ai >正文

深入浅出之STL源码分析6_模版编译问题

1.模版编译原理

当我们在代码中使用了一个模板,触发了一个实例化过程时,编译器就会用模板的实参(Arguments)去替换(Substitute)模板的形参(Parameters),生成对应的代码。同时,编译器会根据一定规则选择一个位置,将生成的代码插入到这个位置中,这个位置被称为 POI(point of instantiation)。由于要做替换才能生成具体的代码,因此 C++ 要求模板的定义对它的 POI 一定要是可见的。换句话说,在同一个翻译单元(Translation Unit)中,编译器一定要能看到模板的定义,才能对其进行替换,完成实例化。

2.包含模型

因此最常见的做法是,我们会将模板定义在头文件中,然后再源文件中 #include 头文件来获取该模板的定义。这就是模板编程中的包含模型(Inclusion Model)。

所以我们一般情况下,对于模版会把定义放在头文件中。但是这样操作会带来缺点。这个也对应着显示实例化的优点。

现在的一些 C++ 库,整个项目中就只有头文件,没有源文件,库的逻辑全部由模板实现在头文件中。而且这种做法似乎越来越流行,在 GitHub 和 boost 中能看到很多很多。我想原因一个是 C++ 缺乏一个官方的 package manager,这样发布的软件包更易使用(include就行了);另一个就是模板实例化的这种要求,使得包含模型成为泛型编程中组织代码最容易的方式。

但包含模型也有自身的问题。在一个翻译单元(Translation Unit)中,同一个模板实例只会被实例化一次。也就是对同一个模板传入相同的实参,编译器会先检查是否已实例化过,如果是则使用之前实例化的结果。但在不同的翻译单元中,相同实参的模板会被实例化多次,从而产生多个相同的类型、函数和变量

这带来两个问题:

2.1 链接时的重定义问题

如果不加以处理,这些相同的实体会被链接器认为是重定义的符号,这违反了ODR(One Definition Rule)。对这个问题的主流解决方案是为模板实例化生成的实体添加特殊标记,链接器在链接时对有标记的符号做特殊处理。例如在 GNU 体系下,模板实例化生成的符号都被标记为弱符号(Weak Symbol)。不需要我们参与,连接器已经为我们解决了这个问题。

对于普通的函数和类,连接器是不会处理的。会报重定义的错误。

同时这个因为每一个编译单元都有实例化,会带来代码膨胀。

2.2 编译时长的问题

同一个模板传入相同实参在不同的编译单元下被实例化了多次,这是不必要的,浪费了编译的时间。

3.分离模型

就是将声明放在头文件,实现放在.cpp中,并对其进行显示实例化,为什么要进行显示实例化,我在后面会详细的介绍。

显示实例化有如下的优点:

1. 降低编译器构建的时间

2.降低代码膨胀

3.针对发布lib,可以隐藏头文件

当然缺点也很明显,你要用到哪个模版类型,你提前事先都得很清楚,并且随着代码的累加,你都要动态的维护。

4.验证前面说的结论

1.如果在翻译单元的编译期间,能够看到模版的定义,就会在当前单元进行实例化。

也就是这个是一个多余的动作,就是能看见就顺便实例化了,调用函数正常。

2.如果在当前翻译单元看不到模版的定义,则只会调用这个函数,也就是认为声明在这个.h文件中,定义不在,链接的时候再去找。

3.正常的编译期就会生成对成员函数的调用,只是能看到模版的定义时,在本翻译单元的时候,才会进行实例化。如果在本翻译单元没有定义,则会在链接阶段再去找定义。

4.只要是实例化一定是在编译阶段,只是在你这个单元是否可见。

以上说的这几点,主要是针对包含模型和分离模型的对比来说的。

下面我们看下具体的例子。

我们先来解释第一句话:

//1.如果在翻译单元的编译期间,能够看到模版的定义,就会在当前单元进行实例化。
//也就是这个是一个多余的动作,就是能看见就顺便实例化了,调用函数正常。
//我们定义写一个main.cpp,再写一个function_template.h,然后在function_template.h中写一个实现的函数模版
// main.cpp
#include <iostream>
//#include "zhang.h"
#include "function_template.h"
int main() {int aa = 12;//Zhang temp;int num = system_latency_get_hardware_time(aa);std::cout<<num<<std::endl;return 0;
}
//function_template.h
template <typename Message>
int system_latency_get_hardware_time(const Message& message) {int a = 16;int b = 17;return a+b;
}
// 编译指令如下:
g++ -std=c++17 main.cpp   -o main
// 编译通过,执行./main
33
// 我们再来看下,分步操作会怎么样,我先生成汇编代码.
g++ -std=c++17 -S main.cpp -o main.s

查看对应的汇编代码,发现在汇编里已经出现了 system_latency_get_hardware_time

的定义

//我们修改 function_template.h的代码
// function_template.h
template <typename Message>
int system_latency_get_hardware_time(const Message& message);
// 然后再进行汇编操作
g++ -std=c++17 -S main.cpp -o main.s

这个时候我们汇编不会报错,正如我们第二点所说.

2.如果在当前翻译单元看不到模版的定义,则只会调用这个函数,也就是认为声明在这个.h文件中,定义不在,链接的时候再去找。

关于这个函数搜索只能找到这一条数据。

而我们现在继续向下,链接会怎么样,应该会报链接错误,我们试试.

// 为了少打几个指令,直接进行一步
g++ -std=c++17 main.cpp   -o main
这个时候,在链接的时候,就会报错g++ -std=c++17 main.cpp   -o main
/tmp/ccgzva3Q.o: In function `main':
main.cpp:(.text+0x26): undefined reference to `int system_latency_get_hardware_time<int>(int const&)'
collect2: error: ld returned 1 exit status

这个错误,我们是可以接受的,因为我们确实没有定义.

那我们想把,模版的定义,放到.cpp中可以吗?

// 在刚才的基础上
// function_template.h
template <typename Message>
int system_latency_get_hardware_time(const Message& message);
// add function_template.cpp
#include "function_template.h"
template <typename Message>
int system_latency_get_hardware_time(const Message& message) {int a = 16;int b = 17;return a+b;
}
// 然后我在编译代码的时候,链接下function_template.cpp是不是就可以了呢?
// g++ -std=c++17 main.cpp  function_template.cpp -o main
/tmp/ccsWDsr1.o: In function `main':
main.cpp:(.text+0x26): undefined reference to `int system_latency_get_hardware_time<int>(int const&)'
collect2: error: ld returned 1 exit status

我们发现还是不行,这个其实很好理解,因为在 另外一个编译单元(一个cpp文件就是一个编译单元), function_template.cpp中,这个不会生成这个函数,因为模版没有被实例话,那么方法来了,我们来进行下显示的实例话。

// function_template.cpp
#include "function_template.h"
template <typename Message>
int system_latency_get_hardware_time(const Message& message) {int a = 16;int b = 17;return a+b;
}
// 显示实例话  这个必须放在定义的后面,在.h和.cpp里无所谓.
template int system_latency_get_hardware_time<int> (const int&);

再进行编译,g++ -std=c++17 main.cpp function_template.cpp -o main

我们发现可以编译通过了,说明在 function_template.cpp中,会生成这个函数,那么我们没有给函数体,函数体是谁呢?很显然是是使用 primary template的函数体,因为执行程序的输出是 33.

到现在为止,1,2,3,4 点我都解释清楚了。

这里要主要一个细节,就是类模版的全特化和函数模版的全特化还有些不同,类模版如果全特化以后,还需要进行实例化才能生成代码,但是函数模版的全特化后,不需要实例化了,直接可以生成代码,具体可以参考我的下一篇博文 深入浅出之STL源码分析5_模版实例化与全特化-CSDN博客

http://www.xdnf.cn/news/5394.html

相关文章:

  • 【Tools】git使用详解以及遇到问题汇总
  • 传感器:从单一感知到智能决策的跨越
  • Java基础(异常2)
  • MCP:重塑AI交互的通用协议,成为智能应用的基础设施
  • 【js基础笔记] - 包含es6 类的使用
  • C++(9):位运算符进阶版
  • 变换炉设备设计:结构优化与工艺集成
  • 使用vue3-seamless-scroll实现列表自动滚动播放
  • 中空电机在安装垂直轴高速电机后无法动平衡的原因及解决方案
  • 26考研——中央处理器_指令流水线_流水线的冒险与处理 流水线的性能指标 高级流水线技术(5)
  • LintCode第4题-丑数 II
  • java笔记06
  • Three.js + React 实战系列 - 联系方式提交表单区域 Contact 组件✨(表单绑定 + 表单验证)
  • 频率学派和贝叶斯学派置信区间/可信区间的区别
  • spark算子介绍
  • 机器视觉开发教程——C#如何封装海康工业相机SDK调用OpenCV/YOLO/VisionPro/Halcon算法
  • 高精地图数据错误的侵权责任认定与应对之道
  • 【PVE】ProxmoxVE8虚拟机,存储管理(host磁盘扩容,qcow2/vmdk导入vm,vm磁盘导出与迁移等)
  • 数据库分库分表实战指南:从原理到落地
  • 1247. 后缀表达式
  • Compose笔记(二十二)--NavController
  • 数值运算的误差估计
  • DAMA车轮图
  • PyCharm软件下载和配置Python解释器
  • 【英语笔记(八)】介词和冠词的分析;内容涵盖介词构成、常用介词用法、介词短语;使用冠词表示不同的含义:不定冠词、定冠词、零冠词
  • 【Java项目脚手架系列】第六篇:Spring Boot + JPA项目脚手架
  • Git初始化相关配置
  • Vue 跨域解决方案及其原理剖析
  • springboot3+vue3融合项目实战-大事件文章管理系统-更新用户密码
  • 【AI提示词】免疫系统思维专家