模板头文件、源文件

当模板声明和定义身处两处文件时的错误分析

头文件

//first.h
#ifndef FIRST_H
#define FIRST_H

template<typename T>
void FUN(T);

#endif

源文件

//first.cpp
#include"first.h"
template<typename T>
void FUN(T parameter)
{
 cout << parameter << endl;
}

以上头文件中声明了一个函数模板,源文件中提供了这个函数模板的定义

题外话: 关于声明和定义
· 名字空间、名字空间的别名:通常都是定义
· 类(类模板)、函数(函数模板)、成员函数(成员函数模板):当这些声明提供了花括号的时候,才是定义,否则都是声明(其余的像类、函数这种形式,也可以应用于这种规则,例如:联合、运算符函数、构造。。。但是命名空间、枚举特殊)
· 枚举类型的花括号中包含枚举子时,才是定义(区别于普通的像类的这种形式只要提供了花括号就是定义)
· 全局变量:没有使用extern修饰的全局变量或者具有一个初始化器的全局变量都是定义(普通情况下,可以认为只有前面附带extern的全局变量才是声明,其余都可以看做定义)——例如:int a;a就是一个定义
· 局部变量、非静态的成员变量:一般都被看做是定义
· 静态成员变量:当这些实体出现在包含他们的类或者类模板的外部时,才会被看做是定义(对比于全局静态变量:static int a;如果a是全局,将被看做定义,如果a是成员静态变量,则是声明)
· typedefs、using-declarations、using-directive:这些不能被看做定义
· 显式实例化的指示符:当做定义看待

好了,整理了一下上面的题外话之后我们来继续整理本片文章的主题:

#include"first.h"
#include<iostream>
using namespace std;

void main()
{
 string str = "test string!";
 FUN(str);
}

对于上面的主程序,只是包含了first的头文件,此时 FUN(str)想通过实参演绎调用FUN函数,但是这里出现了问题:编译的时候没有问题

在这里插入图片描述
但是运行的时候却出现了错误:
在这里插入图片描述
这里将错误的信息粘贴出来:
错误 1 error LNK2019: 无法解析的外部符号: “void __cdecl FUN<class std::basic_string<char,struct std::char_traits,class std::allocator > >(class std::basic_string<char,struct std::char_traits,class std::allocator >)” (?? F U N @ V ? FUN@V? FUN@V?basic_string@DU? c h a r t r a i t s @ D @ s t d @ @ V ? char_traits@D@std@@V? chartraits@D@std@@V?allocator@D@2@@std@@@@YAXV? b a s i c s t r i n g @ D U ? basic_string@DU? basicstring@DU?char_traits@D@std@@V?$allocator@D@2@@std@@@Z),该符号在函数 _main 中被引用

可以看到错误是无法解析的外部符号,这里将发生此错误的一般情况列举一下:

1,lib文件未引入:lib文件是主程序和DLL之间的桥梁,这里没有包含lib文件当然就找不到DLL的位置并且去调用相应的实体
2. 没有使用限定符调用实体:MyClass::Add(100)——Add(100)
3, 缺少 obj 文件
4, 找不到函数定义:

可以看到,这个错误( 无法解析的外部符号: )一般就是由于 链接不到实体: 而产生的:再做一个测试:

在这里插入图片描述上面示例很好理解:由于找不到fun()函数的定义而产生了无法解析的外部符号的错误;
这里关键点在于 链接不到实体 ,就是说,我们本来想通过实例化一个函数实体然后再通过语句访问它,但是却没有达到目的(实例化)——这即是问题所在

问题剖析:

 string str = "test string!";
 FUN(str);

对上面代码进行剖析之前我们先明白实例化一个模板需要哪些条件:
1:编译器需要知道实例化哪个定义
2:编译器需要知道基于哪个模板实参进行实例化

这里 对于FUN(str),编译器知道将要实例化一个FUN函数的定义(编译器可以在first.cpp中找到FUN的模板),但是模板参数(str——>string)却很遗憾没办法传入first.cpp文件中去(第二个条件不满足),因此导致实例化失败!

编译通过的原因: 当编译器看到FUN函数的调用,但是没有看到FUN函数的定义的时候——编译器会假设在别处提供了这个定义,并且会产生一个指向该定义的引用,这是“要命”的地方,编译器这么做了之后,当我们拿着这个被假定可以在某处找到一个定义,然后让此引用访问的时候,但是却找不到(因为实例化是失败的),就会报告使用此引用来引用实体失败——这就是 无法解析的外部命令

针对这种情况的解决办法—— 显式实例化:

既然实例化失败的原因是因为模板实参无法传递到模板的定义,那么如果我们不靠客户端代码传递模板实参,而是在模板定义的源文件中主动指定模板实参,那么问题迎刃而解,即靠手动使模板实例化成功
头文件

//first.h
#ifndef FIRST_H
#define FIRST_H

template<typename T>
void FUN(T);

#endif

源文件

//first.cpp
#include"first.h"
template<typename T>
void FUN(T parameter)
{
 cout << parameter << endl;
}
//显式实例化出一个FUN<string>类型的函数实体
template void FUN<string>(string parameter);

运行结果:
在这里插入图片描述
可以看到此时运行成功——即寻找FUN类型的函数实体成功

但是这种在.cpp文件中预先实例化特定类型的实体的策略是有缺点的,因为用户只能使用由模板设计人员提供的特定类型的实例化体,而不能实例化除此之外的任何实例化体——将代码:template void FUN(string parameter);加入到客户端源文件中是不行的!

但是这种在模板源文件中显式实例化也是有其优点的,即可以精确控制模板实例化体的位置,对于实例化一个模板,编译器在处理将这个实例化体放在什么地方的时候,有它自己的一套策略(POI原则:point of instance),而这种在同一个地方集中实例化可以精确控制模板实例化体的位置!在大型项目中,如果有跟踪实例化体位置的需求,并且项目很大时,将会为随意位置实例化而付出代价!

这种显式实例化的方式有利有弊,设计时需要根据需求取舍!

问题总要方法来解决

1, 包含模型

对于开头部分给出的模板声明和模板定义的分离而导致的问题,包含模型的解决办法就是将声明和实现“包含”起来,即让模板的定义访问到模板实参成功地实例化——(这也是模板在面对模板声明和模板定义分离时候的局限性)

包含模型的三种形式

1,将.cpp文件直接包含进.h文件的末尾——模板使用程序只要包含.h就行了

2,模板使用程序在包含.h的地方同样包含.cpp

3,直接将.h和.cpp合并成一个文件(即.h中即包含模板声明又包含模板定义)——模板使用程序只要包含.h就行了

这种包含模型其实已经接近于直接在dot-C文件中提供模板声明和定义了,但是封装性是直接在dot-C文件中声明定义所有模板所无法比拟的!

包含模型的缺点

包含模型最大的缺点就是包含头文件,即无论上述哪三种形式,在最后的应用中,都会导致将.h或者.cpp中的头文件包含进使用模板的dot-C文件中,而编译这些头文件将增加程序编译额外的开销

包含模型另外一个缺点: 如果有多个文件包含了这些模板文件,然后使用了这些模板(实例化了模板),那么将会存在在多个文件中有同种类型函数的(实例化参数一样)实例化,此时当编译器碰见一种函数类型的多个定义之后就会报错,但是对于这种情况,编译器为我们处理了(贪婪实例化、询问实例化、迭代实例化)

总结:我们可以使用包含模型来解决问题,也可以使用显式实例化来解决问题

(前面只给出了显式实例化的原理,以下给出使用显式实例化的模型:)

头文件

//first.h
#ifndef FIRST_H
#define FIRST_H

template<typename T>
void FUN(T);

#endif

源文件

//first.cpp
#findef FIRST_CPP
#define FIRST_CPP

#include"first.h"
template<typename T>
void FUN(T parameter)
{
 cout << parameter << endl;
}

#endif

一个打包了所有显式实例化的源文件(新建的)

//first_init.cpp
#include"first.cpp"
#include<string>
using namespace std;

template void FUN<string>(string);
template void FUN<>(int);
template void FUN(double);

main文件(dot-C文件)

//main.cpp
#include<iostream>
#include<string>
#include"first.h"
using namespace std;

class X
{
public:
 int a;
};

void main()
{
 string str = "test string!";
 FUN(str);
 FUN(100);
 FUN(3.333);
 //FUN(X());//error:显式实例化文件中没有此类型的显式实例化
}

2,分离模型

export关键字:

如果我们不使用包含模型或者显式实例化来解决问题,而是在模板头文件中给模板声明加上export:

#ifndef FIRST_H
#define FIRST_H

export 
template<typename T>
void FUN(T);

#endif

这样模板FUN就被导出了(尽管模板声明和定义在不同的翻译单元中),使用模板的程序只用包含模板头文件就可以正常使用了

export语法:
1,export如果用于模板,必须在template前面
2,export不能和inline一起联用
3,只需在模板声明的地方加export,定义的位置可以加也可以不加

当export遇见类:
· export可以应用于类模板的成员函数(此成员函数可以是模板也可以是非模板)、类模板的静态数据成员
· 当export作用于类模板的声明,整个类不会被导出,但是类内部的可导出成员都被看做可导出实体——因此为了导出整个模板类,模板类的定义还是要位于声明头文件中

分离模型的限制:
遗憾的是,C++11以前,export这个特性并没有和其余的C++特性那样广为流传(只有很少的编译器厂商对export支持),因此C++程序员使用export的经验也是很少的——而在C++11中直接剔除了export,所以我们这里讨论的export以及分离模型只是对过往的研讨!

export导出模板的缺点:
假如我们使用export导出的模板实例化出一个实体,那么模板被实例化的位置模板定义的位置将是完全分离的——使用包含模型的时候,我们的使用模板的程序包含了模板的声明和定义,那么我们实例化一个模板和模板的定义将可以看做在同一个文件中,而对于只包含了模板声明所在头文件的分离模型来说,因为模板定义.cpp我们没有直接包含,所以导致模板被实例化的位置模板定义的位置在不同的文件,而编译器为了这两个位置建立了一些看不见的耦合——包含模板定义的文件发生了改变,那么不仅该文件需要重新编译,所有对该文件中模板进行实例化的文件都要重新编译
正是由于这种耦合的不可见性,那些基于代码的依赖性管理工具都将不再适用,这将意味着编译器需要进行一些额外的处理,来跟踪所有的这些耦合——这将导致程序的创建时间可能比包含模型更多

另外当导出模板在遇到模板的两阶段查找时将很容易导致 二义性 问题: 遗憾,因为C++11已经摒弃了export并且VS编译器并不支持export,所以没法进行这种二义性问题的展示!

总结:
至此,当我们想将模板的声明和定义的分处两个不同的文件的时候,可以有两种方法参考:
1,包含模型,其缺点是需要包含额外的头文件,而且其并没有实际实现声明和定义文件的分离
2,显式实例化,这种模型有其只能使用设计者提供的有限的实例体的限制,不允许客户端实例化额外的类型——但是实现了文件分离,没有包含额外头文件的开销
(3,export因为其缺陷和不在程序员之间频繁使用,已被C++11摒弃)

所以经过综合,提倡将模板的声明和定义都写在同一个文件中的策略有其道理——虽然增加了额外的包含头文件开销

题外话:对于编译深层次模板的时候遇见语义错误时简化错误报告:
1,语言扩展——直接对模板实参中需要满足的类型要求进行判断

template<typename T>
void shell(T a)
{
//这里对于T应该满足的所有类型要求都进行判断,如果T参数没有满足一些要求
//(即当进行深层次的模板实参语义分析的时候可能会出现的错误)
//那么就抛出异常或者阻止深层次的实例化

next(a);
}

2,提前使用参数(用于测试的哑巴代码)——通过测试的方法进行诊断

template<typename T>
void shell(T a)
{
 template try
 {
  typename T::orderType temp;
  *temp;
 }
 catch "T类型必须有一个orderType类型成员,并且orderType类型成员可以被解引用"

//下面的正常代码

}

(这两种技术的可移植性差、有时候将掩饰一些更高层级不能被捕获的错误!)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柔弱胜刚强.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值