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

深入浅出之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博客

相关文章:

  • Kubernetes .yaml 文件配置
  • Kubernetes 集群部署应用
  • C PRIMER PLUS——第9节:动态内存分配、存储类别、链接和内存管理
  • 17前端项目----支付弹框
  • Three.js + React 实战系列 - 页脚区域 Footer 组件 ✨
  • vector--OJ1
  • Windows系统更新一键禁用:WindowsUpdateBlocker轻量级工具推荐
  • Typescript 源码核心流程
  • LeetCode 热题 100 101. 对称二叉树
  • 79.评论日记
  • UOJ 164【清华集训2015】V Solution
  • 元数据分类
  • 并发笔记-给数据上锁(二)
  • 怎样选择成长股 读书笔记(一)
  • Linux epoll 详解:概念、使用、数据结构、流程及应用
  • 力扣-二叉树-101 对称二叉树
  • 常见的 DCGM 设备级别指标及其含义
  • 一个网球新手的学习心得
  • 【C语言文件操作详解】fopen 函数全解析 —— 模式参数、使用技巧与重定向的区别
  • 运动员技术等级分为国际级运动健将
  • 减重人生|走过节食弯路,她如何半年减60斤找回自信?
  • 巴基斯坦全面恢复领空开放
  • 洗冤录|县令遇豪强:黄榦处理的一起地产纠纷案
  • 明查|哈佛大学批改美教育部长来信,红笔标出语法错误?
  • 罕见沙尘再度入川,官方:沙尘传输高度达到平流层,远超以往
  • 英国和美国就关税贸易协议条款达成一致