问题描述
按照C++语言的习惯,普通函数以及类的声明应该放在一个头文件中,而将其实现放在一个主代码问中,这样便于将代码分散编译到多个模板文件中,最后通过链接形成一个完整的模板文件。但是由于模板的实现是随用随生成,并不存在真正的函数实现代码,如果还是按照“头文件放声明,主文件放实现”的做法,则会导致编译失败。
看个例子:
- myfirst.h
#pragma once
template<typename T>
void print_typeof(T const &);
- myfirst.cpp
#include "myfirst.h"
#include <typeinfo>
#include <iostream>
template<typename T>
void print_typeof(T const &x){
std::cout << typeid(x).name() << std::endl;
}
- main.cpp
#include "myfirst.h"
int main(){
double i = 3.0;
print_typeof(i);
}
链接器报错:
void print_typeof<double const &>
即print_typeof函数模板的某个实例未定义。- 理论上这样一个函数的实现应该是在myfirst.cpp编译出的目标文件myfirst.o中定义,但是如果查看myfirst.o就会发现其中什么也没有
原因分析
- 编译器在编译myfirst.cpp时,只是读到了print_typeof函数模板的实现,并没有读到任何需要生成函数模板实例的语句,所以不会生成任何print_typeof函数的实例
- 在编译mian.cpp时,虽然用到了一个函数模板实现,但是main.cpp只是将myfirst.h头文件包含了进来,而后者只有一个print_typeof函数模板的声明,并无具体函数的实现,此时编译器也无法生成print_typeof函数的实例,只好预留一个调用链接,即假设在别处提供了这个定义,并且产生一个指向该定义的引用。但是这样的链接不存在,于是最后链接时出错
解决方案
解决方案:让声明和定义位于同一个同文件中
- myfirst.h
#pragma once
#include <typeinfo>
#include <iostream>
template<typename T>
void print_typeof(T const &);
template<typename T>
void print_typeof(T const &x){
std::cout << typeid(x).name() << std::endl;
}
- main.cpp
#include "myfirst.h"
int main(){
double i = 3.0;
print_typeof(i);
}
模板的这种组织方式叫做包含模型
- 优点:包含模型能够确保所有需要的模板都已经实例化。这时因为,当需要进行实例化的时候,C++编译系统会自动产生所对应的实例化体。
- 缺点:包含模型明显增加了包含头文件的
myfirst.hpp
的开销----》主要的开销并不是取决于模板定义本身的大小,而是在于模板定义中所包含的那些头文件(比如#include <typeinfo>#include <iostream>
),这会带来成千上万的代码,大大增加了编译复杂程序所耗费的时间。 - 评价:尽量使用包含模型的方式来组织模板代码,因为其他的解决方式更烂
解决方法2:
- myfirst.h
#pragma once
#include <typeinfo>
#include <iostream>
template<typename T>
inline void print_typeof(T const &x){
std::cout << typeid(x).name() << std::endl;
}
- main.cpp
#include "myfirst.h"
int main(){
double i = 3.0;
print_typeof(i);
}
解决方法3:明确生成模板实例:
- 当template后没有模板参数列表,而是一个函数声明时,意味着指示编译器根据此函数声明寻找合适的模板实现。当然,所声明的函数必须与某一已知模板函数同时,并且其参数与可用模板匹配
- 不推荐,如果有了其他类型的,就需要增加相应的语句,这样一来,又变成人工生成模板实例,违背了当初由编译器随用随生成的初衷
修改myfirst.cpp
#include "myfirst.h"
#include <typeinfo>
#include <iostream>
template<typename T>
void print_typeof(T const &x){
std::cout << typeid(x).name() << std::endl;
}
template void print_typeof(double const &x);
从包含模型得出的另一个结论是:
- 非内联函数模板与“内联函数和宏”有一个很重要的区别,那就是非内联函数模板在调用的时候不会被扩展,而是当它们基于某种类型进行实例化之后,才产生一份新的(基于该类型)的函数拷贝。
- 因为产生函数拷贝是一个自动化过程,所以在编译结束的时候,编译器可能会在不同的文件里产生两份拷贝。
- 于是,当链接器发现同一个函数具有两种不同的定义时,就会报告一个错误。
- 实际上,这是C++的编译系统来解决的问题,我们不必太关心,只有在创建自身代码库的大项目时才需要考虑这个问题
另外:
- 虽然模板中的函数也可以由自己的声明和实现,但编译器不会在读到模板实现时立即生成实际代码,因为具体的模板参数类型还不知道,无法进行编译。
- 对于编译器来说,模板实现也是一种声明,声明如何自动生成代码。
- 所以模板的实现也应该放在头文件内,这样,在其他代码文件中可以直接将模板的实现也包含进来。当需要生成模板实例的时候,编译器可根据已知模板实现当场生成,而无需依赖在别的模板中生成的模板实例
- 但这样会带来另一个问题,即重复模板实例
重复模板实例
两个函数由同一模板生成,完全等价,则这两个函数为重复模板实例
如果在最后链接步骤不做特殊处理,则会在最终目标代码中存在多个等价的模板实例,造成目标文件的增加。对此,C++标准中给出的解决方案是:在链接时识别以及合并等价的目标实例
那么,链接器如何识别等价的目标实例呢?
我们来看个例子:
- caller1.cpp
#include <iostream>
template <typename T>
void func(T const &v){
std::cout << "func1:" << v << "\n";
};
void caller1(){
func(1);
func(0.1);
};
- caller2.cpp
#include <iostream>
template <typename T>
void func(T const &v){
std::cout << "func2:" << v << "\n";
};
void caller2(){
func(2);
func(0.2);
};
- main.cpp
void caller1();
void caller2();
int main(){
caller1();
caller2();
return 0;
}
由上面推知:链接器在链接时不考虑函数具体内容1,仅仅通过函数名、目标实参列表以及参数列表等“接口”信息判断两个函数是否等价。
实际上,编译器在编译函数模板实例时,将根据函数名、函数参数类型以及模板参数等信息来重命名编译所生成的模板函数名。这一处理方式叫做Name-Mangling。如果发现“接口”等价的函数(即编译后的函数名相同),则在最终可执行代码中只保留等价函数之一作为链接候选,而放弃其他等价函数。具体保留哪个函数是随机的,可能与用户输入有关。