函数加载的问题记录
一、发生情况
今天在学习模板的时候,尝试着把模板的声明和定义进行分开操作:
定义头文件temp.h
// temp.h
#include<iostream>
using namespace std;
template<typename T>
bool compare(T a, T b);
定义源文件temp.cpp
//temp.cpp
#include "temp.h"
template<typename T>
bool compare(T a, T b) {
cout << "模板函数template compare" << endl;
return a > b;
}
template<>
bool compare<const char*>(const char* a, const char* b) {
cout << "模板特例化compare<const char*>" << endl;
return strcmp(a, b) > 0;
}
bool compare(const char* a, const char* b) {
cout << "普通函数normal function()" << endl;
return strcmp(a, b) > 0;
}
定义源文件main.cpp
#include "temp.h"
#include <iostream>
using namespace std;
int main() {
bool flag = 1;
/*
//此处定义的代码都会error。因为无法实例化对应的模板
flag = compare<int>(5, 76);
cout << flag << endl;
flag = compare(41233.35, 452.1);
cout << flag << endl;
flag = compare<std::string>("sdfga", "asfas");
cout << flag << endl;
*/
flag = compare("sdfga", "asfas");
cout << flag << endl;
flag = compare<const char *>("sdfga", "asfas");
cout << flag << endl;
return 0;
}
二、疑问
- 今天学习函数模板的时候发现了模板特例化和函数重载的调用优先级有所区别。
- 当模板声明就在main函数中的时候,会优先使用普通函数,然后才是模板特例化
- 当模板的声明和定义被分别放在头文件和源文件的时候,然后在main文件里面引入头文件,会发现无法加载通用模板以及普通函数,只能加载其特例化。
- 那么此时问题来了:居然无法加载
compare<int
>以及compare<double>
类型,但是为什么可以加载compare<const char *>
类型?- 答:从代码上面来看是因为我提供了
const char*
的特例化。
- 答:从代码上面来看是因为我提供了
- 那么又问:如果在分文件的情况下main只能调用显示指定的函数,那么正常使用的情况下(模板的声明和定义都放在头文件中),编译器又是如何知道我后面需要使用
compare<int
>以及compare<double>
这种的?- 答:当正常使用的时候,会通过头文件把模板的定义也加载到了main.cpp中。这样的话编译器就可以通过这个定义来加载函数模板了。
- 为什么不能加载普通函数,而是只加载其特例化?
- 普通函数在定义时就会生成代码。如果函数定义在一个源文件中(如 temp.cpp),那么除非有相应的声明在头文件中(如 temp.h),否则该函数在其他源文件中是不可见的。
- 因此在头文件temp.h去进行声明就会调用普通函数
bool compare(const char* a, const char* b);
- 那么此时问题来了:居然无法加载
三、函数优先级
- 名称查找:编译器首先查找所有与函数调用名称匹配的函数声明。
- 候选函数集:编译器从名称查找的结果中筛选出与函数调用参数数量和类型相匹配的函数声明,形成一个候选函数集。这个集合可能包含普通函数、模板函数和模板特化的声明。
- 最佳匹配:编译器从候选函数集中选择最佳匹配的函数来调用。在选择过程中,编译器会按照以下顺序进行:
- 完全匹配:如果候选函数集中有与函数调用参数完全匹配的函数(包括类型、数量、const/volatile修饰符等),则选择该函数。这里的“完全匹配”不仅限于普通函数,也包括模板特化(如果模板特化的参数类型与函数调用参数类型完全匹配)。
- 非模板函数优先:如果候选函数集中有多个函数都匹配,但其中一个或多个是模板函数,而至少有一个是非模板函数(即普通函数),则编译器会优先选择非模板函数。
- 模板特化优先:如果候选函数集中有多个模板函数,并且其中一个是模板特化(针对某个特定类型或一组类型进行了特化),则编译器会优先选择模板特化。
- 模板参数推导:如果候选函数集中有多个模板函数,并且没有模板特化,编译器会尝试对每个模板函数进行模板参数推导。推导成功的模板函数会被添加到候选函数集中。然后,编译器会根据推导结果的“具体性”来选择最佳匹配。
- 错误处理:如果编译器在候选函数集中找不到最佳匹配的函数,或者找到了多个同样最佳的函数(这通常被称为歧义),则会报告一个编译错误。
四、模板的编译和链接过程
1.模板的编译
当编译器编译一个包含模板定义的源文件(如test.cpp
)时,它并不生成具体的代码。模板代码本身只是一个蓝图或者说是一种生成代码的指令。
2.模板的实例化
-
隐式实例化:
当你在代码中使用模板时,如compare<int>(5, 76)
,如果当前编译单元(源文件)可以访问到模板的定义,编译器会自动为这个特定类型(这里是int)生成模板的实例。
比如在template.cpp
文件中有一个函数,改函数使用了compare<int>(3,12)
。template<typename T> bool compare(T a, T b) { cout << "template compare" << endl; return a > b; } void addition() { comp(4, 5);//此时会隐式的产生compare<int>的实例 }
-
显示实例化
你可以在一个源文件中显式地指定模板的实例化,如template bool compare<int>(int, int);
。这会强制编译器在该文件中生成此模板的实例。template<typename T> bool compare(T a, T b) { cout << "template compare" << endl; return a > b; } template bool compare<int>(int, int); template bool compare<double>(double, double); template bool compare<std::string>(std::string, std::string);
3.模板的链接
在链接阶段,链接器需要解析所有外部符号引用,以确保程序的各个部分正确连接在一起。如果main.cpp
中调用了compare<double>()
,而这个实例只在test.cpp
中声明(没有实际生成),且test.cpp
中也没有显式实例化这个模板,那么链接器将无法找到compare<double>()
的定义,从而导致“无法解析的外部符号”错误。