众所周知c++的模板定义通常实现在头文件中,当然也可以实现在源文件中,但这是不推荐的做法,今天就结合一下符号表来看一下为什么不推荐这样做。
标准用法
先来看一个标准模板实现的例子:
template.h
#ifndef _TEMP_H_
#define _TEMP_H_
template<typename T>
T maxx(T t1,T t2)
{
return t1<t2?t2:t1;
};
#endif
main.cc
#include "template.h"
#include <iostream>
using namespace std;
int main()
{
int a=1,b=2;
cout<<maxx(a,b)<<endl;
return 0;
}
这种写法当然是可以正常编译和链接的:
$ g++ -c main.cc
$ g++ main.o -o main
来看一下main.o的符号表:
可以通过nm、objdump、readelf等多种工具查看,这里选用的是nm,且为了聚焦问题只列出我们关注的符号:
$ nm -C main.o
0000000000000000 W int maxx<int>(int, int)
可以看到,maxx此时实例化出了一个maxx版本,其是一个弱符号,这是因为模板定义在头文件中,而main.cc直接包含了头文件,所以对main.cc编译时便可以找到模板的实现从而实例化出对应的版本。
普通函数的声明与定义分离
为了引出模板的声明与定义分离的问题,我们先来看一下普通函数:
test.h
#ifndef _TEST_H_
#define _TEST_H_
void fun();
#endif
test.cc
#include <iostream>
#include "test.h"
void fun()
{
std::cout<<"for test"<<std::endl;
}
main.cc
#include "test.h"
int main()
{
fun();
return 0;
}
$ g++ -c test.cc
$ g++ -c main.cc
$ g++ test.o main.o -o test
$ nm -C main.o
0000000000000000 T main
U fun()
$ nm -C test.o
0000000000000000 T fun()
$ nm -C test
000000000040081f T fun()
可以看到,在main.cc编译时由于看不到fun()的实现,所以此时fun是一个undefined的符号,而在test.o中,fun()已经是存放在了代码段,在经过链接生成可执行文件之后,连接器从test.o中找到了fun()的实现,存放在了放在了可执行文件test的代码段,整个过程是没有问题的
声明与定义分离:不可行
接下来看下如果不是普通函数而是模板函数,有什么区别?
template.h
#ifndef _TEMP_H_
#define _TEMP_H_
template<typename T>
T maxx(T t1,T t2);
#endif
template.cc
#include "template.h"
template<typename T>
T maxx(T t1,T t2)
{
return t1<t2?t2:t1;
};
main.cc
#include "template.h"
#include <iostream>
using namespace std;
int main()
{
int a=1,b=2;
cout<<maxx(a,b)<<endl;
return 0;
}
$ g++ -c template.cc
$ g++ -c main.cc
$ g++ -o main template.cc main.cc
其中,编译过程没有问题,但是链接时则会报错,找不到int maxx(int, int)的定义
/tmp/ccFNxw0L.o:在函数‘main’中:
main.cc:(.text+0x21):对‘int maxx<int>(int, int)’未定义的引用
collect2: 错误:ld 返回 1
这是为什么?再次分别看下template.o和main.o的符号表:
$ nm -C main.o
0000000000000000 T main
0000000000000045 t __static_initialization_and_destruction_0(int, int)
U int maxx<int>(int, int)
可以看到,和普通函数的声明与定义分离相同,这里的maxx也是未定义的符号。那么如果像普通函数一样在链接时可以在其他.o中找到需要的符号也是没问题的,但为什么这里链接报错了?
因为实际上template.o的符号表是完全为空的,这就导致在链接时根本找不到 int maxx(int, int),也就发生了上面的链接报错。而template.o的符号表为空的原因则是C++标准对模板的的相关规定,也是模板推荐直接定义在头文件中的根本原因:模板只有在被调用的时候才会被实例化!
在上面的例子中main.cc虽然调用了,但是其编译时找不到模板定义,无法实例化,template.cc有可以找到模板定义但是没有调用,所以在链接时所有.o中都找不到maxx的符号,所以链接出错。这也就是通常不推荐模板声明与定义分离的原因。
声明与定义分离+定义处调用:局限性可用
通过前面的分析可以看出来我们如果在template.cc中调用一次maxx,那么其编译时便会实例化出来一个版本,main.cc中也就可以调用了,实际验证下:
template.cc
#include "template.h"
template<typename T>
T maxx(T t1,T t2)
{
return t1<t2?t2:t1;
};
void f(int a,int b)
{
maxx(a,b);
}
$ nm -C template.o
0000000000000000 T f(int, int)
0000000000000000 W int maxx<int>(int, int)
$ g++ template.o main.o -o main
确实和预期相同,此时链接正常,运行正常。
由此可见,只要在template.cc中有调用maxx模板函数,那么template.o中就会实例化出一个对应的版本,即使这个调用所在的函数和main毫无关联。但对main.o来说,只要链接的时候能找到对应版本的maxx符号就可以了。
那么很自然的能想到这种方式的局限性,template.cc中的调用只会实例化maxx<int,int>版本,如果main.cc中的调用不是这个版本的,那么这种方式还可行吗?
修改main.cc如下:
#include "template.h"
template<typename T>
T maxx(T t1,T t2)
{
return t1<t2?t2:t1;
};
void f(int a,int b)
{
maxx(a,b);
}
[root@10-10-112-54 template]# cat main.cc
#include "template.h"
#include <iostream>
using namespace std;
int main()
{
unsigned int a=1,b=2;
cout<<maxx(a,b)<<endl;
return 0;
}
$ g++ -c template.cc
$ g++ -c main.cc
$ g++ -o main main.cc template.cc
/tmp/cccZHqZv.o:在函数‘main’中:
main.cc:(.text+0x21):对‘unsigned int maxx<unsigned int>(unsigned int, unsigned int)’未定义的引用
collect2: 错误:ld 返回 1
答案显然是不可行的,只是将int改为了unsigned int,此时再编译,链接便会报错,因为此时的main.o中maxx的符号已经变为了unsigned int版本,而template.o中只有int版本的,那链接时自然找不到想要的便会出错。
U unsigned int maxx<unsigned int>(unsigned int, unsigned int)
总结
从前面的几个例子可以看出来,模板的声明与定义分离也是支持的,但是有很大的局限性而且看起来逻辑不清晰,直接在头文件中定义模板实现是最好的方式。而根本原因就是C++模板只在调用时才会被实例化这一规定。