从汇编角度看为什么模板函数声明和定义要放在头文件上

下面纯属个人理解,请理智看待


吐槽:其实标题不算太对.

1.头文件其实没什么作用,头文件大多只是给予开发人员在开发的时候的一种方便查找接口声明,或者方便组内开发的规范,或者模块化定义。

其实头文件仅仅是使用在预处理的情况下。而预处理的工作也仅是将头文件里的信息拷贝到cpp文件里面进而生成预处理的文件。

2.模板函数的声明和定义其实可以分开- -,只是这样做没什么意义了。


下面我们完整的编译链接一个模板函数的程序

fun.h文件:

#ifndef FUN_H_
#define FUN_H_
template<typename T>
T add(T a, T b) {
    return a + b;
}
#endif
main.cpp文件

#include "fun.h"

int main() {
    add(1, 3); 
    return 0;
}
1.预处理g++ -E main.cpp

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "main.cpp"
# 1 "fun.h" 1


template<typename T>
T add(T a, T b) {
    return a + b;
}
# 2 "main.cpp" 2

int main() {
    add(1, 3);
    return 0;
}
可以看到预处理的文件,是直接将fun.h的头文件信息直接拷贝了过来。

2.编译g++ -S main.cpp,生成汇编代码

    .file   "main.cpp"
    .text
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16 
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $3, %esi
    movl    $1, %edi
    call    _Z3addIiET_S0_S0_
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret 
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .section    .text._Z3addIiET_S0_S0_,"axG",@progbits,_Z3addIiET_S0_S0_,comdat
    .weak   _Z3addIiET_S0_S0_
    .type   _Z3addIiET_S0_S0_, @function
_Z3addIiET_S0_S0_:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16 
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -8(%rbp), %eax
    movl    -4(%rbp), %edx
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret 
    .cfi_endproc
.LFE2:
    .size   _Z3addIiET_S0_S0_, .-_Z3addIiET_S0_S0_
    .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
    .section    .note.GNU-stack,"",@progbits
为了可以更好的查看,我们可以进行汇编成.o文件再进行objdump查看

g++ -c main.s && objdump -S main.o

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	be 03 00 00 00       	mov    $0x3,%esi
   9:	bf 01 00 00 00       	mov    $0x1,%edi
   e:	e8 00 00 00 00       	callq  13 <main+0x13>
  13:	b8 00 00 00 00       	mov    $0x0,%eax
  18:	5d                   	pop    %rbp
  19:	c3                   	retq   

Disassembly of section .text._Z3addIiET_S0_S0_:

000000000000001a <_Z3addIiET_S0_S0_>:
  1a:	55                   	push   %rbp
  1b:	48 89 e5             	mov    %rsp,%rbp
  1e:	89 7d fc             	mov    %edi,-0x4(%rbp)
  21:	89 75 f8             	mov    %esi,-0x8(%rbp)
  24:	8b 45 f8             	mov    -0x8(%rbp),%eax
  27:	8b 55 fc             	mov    -0x4(%rbp),%edx
  2a:	01 d0                	add    %edx,%eax
  2c:	5d                   	pop    %rbp
  2d:	c3                   	retq   
这里的edx和eax都是32位寄存器,再结合_Z3addIiET_S0_S0_可以推断出是int形add函数的实现( 其实可以直接通过c++filt进行查看,可得到int add<int>(int, int))。

从这里可以知道main.cpp在编译成.o文件的时候,其是先将头文件的方法声明和定义都拷贝了下来,然后再结合main方法内部的add(1,3)来联系上下文,可以知道

会有一个int的add实现需要生成。这看起来很自然。

分割线----------------------------------------------------------------------------------------------------------

那么下面我们来看分开的:

fun.h

#ifndef FUN_H_
#define FUN_H_
template<typename T>
T add(T a, T b); 
#endif
main.cpp

#include "fun.h"

int main() {
    add(1, 3); 
    return 0;
}
fun.cpp

#include "fun.h"

template<typename T>
T add(T a, T b) {
    return a + b;
}
1.预处理:g++ -E main.cpp

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "main.cpp"
# 1 "fun.h" 1


template<typename T>
T add(T a, T b);
# 2 "main.cpp" 2

int main() {
    add(1, 3);
    return 0;
}
同上,也是直接拷贝头文件信息

2:查看汇编代码(objdump .0文件):

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	be 03 00 00 00       	mov    $0x3,%esi
   9:	bf 01 00 00 00       	mov    $0x1,%edi
   e:	e8 00 00 00 00       	callq  13 <main+0x13>
  13:	b8 00 00 00 00       	mov    $0x0,%eax
  18:	5d                   	pop    %rbp
  19:	c3                   	retq
现在我们职能看到callq 13 <main+0X13> 对应的二进制e8 00 00 00 00,意思是这个函数调用的地址,在链接(ld)的时候在从其他.o文件中进行寻找,找到的话便会将

这个地址重定向到那个位置上去

3.我们来编译下fun.cpp文件,g++ -S fun.cpp

.file   "fun.cpp"
    .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
    .section    .note.GNU-stack,"",@progbits
是不成功的,其实想想也能理解,就只给你fun.cpp和fun.h文件,根本没有应用场景,又该如何生成具体类型的实例呢,模板只是将通用的方法流程抽象出来,在编写代码的时候

不用在根据多个类型进行重复的编码,只是这样的工作转交给编译工具而已,实际上最终生成的代码可能不会有什么变化。

既然无法生成具体实例的.o文件,那么在链接的时候就注定main.o文件的callq无法重定向到具体的地址,便会报ld错误,没有具体引用实例undefined reference to `int add<int>(int, int)'

4.如果我们在fun.cpp特例化一个int add(int a, int b)

#include "fun.h"

template<typename T>
T add(T a, T b) {
    return a + b;
}

template<>
int add(int a, int b) {
    return a + b;
}
会发现是可以成功编译成.o文件的,objdump -S fun.o查看汇编代码:

0000000000000000 <_Z3addIiET_S0_S0_>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	89 7d fc             	mov    %edi,-0x4(%rbp)
   7:	89 75 f8             	mov    %esi,-0x8(%rbp)
   a:	8b 45 f8             	mov    -0x8(%rbp),%eax
   d:	8b 55 fc             	mov    -0x4(%rbp),%edx
  10:	01 d0                	add    %edx,%eax
  12:	5d                   	pop    %rbp
  13:	c3                   	retq
生成了一个int add(int a, int b)的函数实现

4.链接成可执行文件:g++ fun.o main.o成功,再次使用objdump -S a.out

0000000000400501 <main>:
  400501:	55                   	push   %rbp
  400502:	48 89 e5             	mov    %rsp,%rbp
  400505:	be 03 00 00 00       	mov    $0x3,%esi
  40050a:	bf 01 00 00 00       	mov    $0x1,%edi
  40050f:	e8 d9 ff ff ff       	callq  4004ed <_Z3addIiET_S0_S0_>
  400514:	b8 00 00 00 00       	mov    $0x0,%eax
  400519:	5d                   	pop    %rbp
  40051a:	c3                   	retq   
  40051b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
直接查看main函数,可以发现callq 4004ed <_Z3addIiET_S0_S0_,e8 d9 ff ff ff,函数地址已经由之前的00 00 00 00更改为d9 ff ff ff,因此在./a.out可以正常执行。

5.因此将模板函数的声明和定义是可以分开来存放的

只是这样是没有意义的,因为你需要特例出当前使用者可能使用的所有类型情况,那么便要为这些类型特例相同的代码,这样就相当于将编译器的工作又交回给我们了。

因此,不如直接将声明和定义直接写在头文件上,以此交由编译器根据上下文实例化需要的函数,以此减轻重复编码的工作。

end.







  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值