原理:C++为什么一般把模板实现放入头文件
写在前面
本文通过实例分析与讲解,解释了为什么C++一般将模板实现放在头文件中。这主要与C/C++的编译机制以及C++模板的实现原理相关,详情见正文。同时,本文给出了不将模板实现放在头文件中的解决方案。
正文
例子
现有如下3个文件:

1 // add.h
2 template <typename T>
3 T Add(const T &a, const T &b);
4
5 // add.cpp
6 #include "add.h"
7
8 template <typename T>
9 T Add(const T &a, const T &b)
10 {
11 return a + b;
12 }
13
14 // main.cpp
15 #include "add.h"
16 #include <iostream>
17
18 int main()
19 {
20 int res = Add<int>(1, 2);
21 std::cout << res << "\n";
22 return 0;
23 }
示例代码
现象
使用 g++ -c add.cpp 编译生成 add.o ,使用 g++ -c main.cpp 编译生成 main.o ,这两步都没有问题。
使用 g++ -o main.exe main.o add.o 生成 main.exe 时,报错 undefined reference to 'int Add(int const&, int const&)' 。
当然,直接 g++ add.cpp main.cpp -o main.exe 肯定也会报错,这里把编译和链接分开是为了更好地展示与分析问题。
原因
出现上述问题的原因是:
(1)C/C++源文件是按编译单元(translation unit)分开、独立编译的。所谓translation unit,其实就是输入给编译器的source code,只不过该source code是经过预处理(pre-processed,包括去掉注释、宏替换、头文件展开)的。在本例中,即便你使用 g++ add.cpp main.cpp -o main.exe ,编译器也是分别编译 add.cpp 和 main.cpp (注意是预处理后的)的。在编译 add.cpp 时,编译器根本感知不到 main.cpp 的存在,反之同理。
(2) C++模板是通过实例化(instantiation)来实现多态(polymorphism)的。以函数模板为例,首先需要区分“函数模板”和“模板函数”。本例中,上面代码的第8~12行是函数模板,顾名思义,它就是一个模子,不是具体的函数,是不能运行的;当用具体的类型,如 int ,实例化模板参数 T 后,会生成函数模板的一个具体实例,称为模板函数,这是真正可以运行的函数。“函数模板”和“模板函数”的关系,可以类比“类”和“对象”的关系。以 int 为例,生成的实例/模板函数大概长这样(细节上肯定和编译器的实际实现有出入,但核心意思不会变)。

1 int Add_int_int(const int &a, const int &b)
2 {
3 return a + b;
4 }
模板函数(示例)
对于每一个用到的具体类型,编译器都会生成对应版本的实例,当函数调用时,会调用到该实例。如用到了 Add<int> ,就会生成 Add_int_int ,用到了 Add<double> ,就会生成 Add_double_double ,等等。本例中,当编译器编译到第20行,即 int res = Add<int>(1, 2); 一句时,编译器就会试图生成 int 版本的模板实例(即模板函数)。
(3)编译器为模板生成实例的必要条件是:1. 知道模板的具体定义/实现;2. 知道模板参数对应的实际类型。
分析
下面把上面两节内容结合起来分析。
(1)当编译 add.cpp 时,相当于编译

1 template <typename T>
2 T Add(const T &a, const T &b);
3
4 template <typename T>
5 T Add(const T &a, const T &b)
6 {
7 return a + b;
8 }
预处理后的add.cpp
此时编译器虽然知道模板的具体定义,却不知道模板参数 T 的具体类型,因此不会生成任何的实例化代码。
(2)当编译 main.cpp 时,相当于编译

1 #include <iostream>
2
3 template <typename T>
4 T Add(const T &a, const T &b);
5
6 int main()
7 {
8 int res = Add<int>(1, 2);
9 std::cout << res << "\n";
10 return 0;
11 }
预处理后的main.cpp
当编译到 int res = Add<int>(1, 2); 时,编译器想要生成 int 版本的函数实例,但它找不到函数模板的具体定义(即 Add 的“函数体”),只好作罢。好在编译器看到了函数模板的声明,于是通过了编译,将寻找 int 版本函数实例的任务留给了链接器。
至此,编译 add.cpp 时,只知模板定义,不知模板类型参数,无法生成具体的函数定义;编译 main.cpp 时,只知模板类型参数,不知模板定义,同样无法生成具体的函数定义。
(3)没什么好说的,链接器在 add.o 和 main.o 中都没找到 int 版本的 Add 定义,直接报错。
解决方案
方案一
传统方法:把模板实现也放在头文件中。

1 // add.h
2 template <typename T>
3 T Add(const T &a, const T &b)
4 {
5 return a + b;
6 }
7
8 // main.cpp
9 #include "add.h"
10 #include <iostream>
11
12 int main()
13 {
14 int res = Add<int>(1, 2);
15 std::cout << res << "\n";
16 return 0;
17 }
解决方案一
当编译 main.cpp 时,相当于编译

1 #include <iostream>
2
3 template <typename T>
4 T Add(const T &a, const T &b)
5 {
6 return a + b;
7 }
8
9 int main()
10 {
11 int res = Add<int>(1, 2);
12 std::cout << res << "\n";
13 return 0;
14 }
预处理后的main.cpp
此时编译器既知道函数模板的定义,又知道具体的模板类型参数 int ,因此可以生成 int 版本的函数实例,不会出错。
这种方式的优缺点如下:
- 优点:可以按需生成。假如我们在 main.cpp 中调用了 Add<double>(1.0, 2.0); ,编译器就会为我们生成 double 版本的函数实例。
- 缺点:不得不把实现细节暴露给用户。
方案二
模板声明和定义分离的方案。

1 // add.h
2 template <typename T>
3 T Add(const T &a, const T &b);
4
5 // add.cpp
6 #include "add.h"
7
8 template <typename T>
9 T Add(const T &a, const T &b)
10 {
11 return a + b;
12 }
13
14 template int Add(const int &a, const int &b);
15
16 // main.cpp
17 #include "add.h"
18 #include <iostream>
19
20 int main()
21 {
22 int res = Add<int>(1, 2);
23 std::cout << res << "\n";
24 return 0;
25 }
解决方案二
注意, template int Add(const int &a, const int &b); 是函数模板实例化(function template instantiation)[1], template 关键字不能省略,否则, int Add(const int &a, const int &b); 会被编译器当做普通函数的声明,从而在链接时又会报 undefined reference to 'int Add(int const&, int const&)' 错误。
对于这种写法,编译器在编译 add.cpp 时,既能看到函数模板的定义,又能看到具体的模板类型参数 int ,于是生成了 int 版本的函数实例,整个程序可以正常编译运行。
很显然,这种情况下编译器只生成了 int 版本的函数实例,所以,在 main.cpp 中使用 Add<double>(1.0, 2.0); 这样的代码肯定是不可以的。这种情况的优缺点可以辩证看待:
- 优点:1. 可以隐藏实现细节(我们可以把 add.cpp 做成.lib或.dll);2. 也可以限制只实例化特定的版本。
- 缺点:就是只能使用特定的几个版本,不能像方案一那样在编译 main.cpp 时根据具体的调用情况按需生成。
从这里也可以看出,模板实现不一定非得放在头文件中。
参考
[1] Function template - cppreference.com
[2] c++ - Why can templates only be implemented in the header file? - Stack Overflow
写在后面
本文从C/C++编译机制以及C++模板实现原理的角度,结合具体实例,讲解了为什么一般将模板实现放在头文件中。由于在下才疏学浅,能力有限,错误疏漏之处在所难免,恳请广大读者批评指正,您的批评是在下前进的不竭动力。
原理:C++为什么一般把模板实现放入头文件的更多相关文章
- JavaScript 将多个引用(样式或者脚本)放入一个文件进行引用
1.将样式放入一个文件进行引用 @import url("../media/css/bootstrap.min.css"); @import url("../media/ ...
- NX二次开发-UFUN将实体放入STL文件中函数UF_STD_put_solid_in_stl_file
NX9+VS2012 #include <uf.h> #include <uf_obj.h> #include <uf_modl.h> #include <u ...
- 【C#操作Excel】同名Excel放入同一文件夹中,然后合并为同一个Excel文件
近期有对Excel操作的需求,由于都是重复劳动,故分享代码如下,本人也是技术菜鸟没有考虑性能,如果有大牛能够指教就再好不过了 事先电脑中需要安装Excel,然后Vs中引用Microsoft.Offic ...
- C# 让你解决方案乱七八糟的DLL放入指定文件夹
嗯,大家的解决方案可能会有许多dll,这样不美观,而且也麻烦. 很多小白都不知道如何将这些dll放到如自己程序的bin文件夹下. 本渣今天来试着将dll复制到指定的文件夹下~ 比如我之前做的一个Win ...
- Json文件放入Assets文件,读取解析并且放入listview中显示。
package com.lixu.TestJson; import android.app.Activity; import android.content.Context; import andro ...
- 导出pip安装的所有放入一个文件中,并把通过这个安装所有的包
导出pip安装的所有的包: pip freeze > piplist.txt 在新的环境中安装导出的包 pip install -r piplist.txt
- django中多个app放入同一文件夹apps
开发IDE:pycharm 新建一个apps文件夹 需要整理的app文件夹拖到同一个文件夹中,即apps.(弹出对话框,取消勾选Search for references) 在pycharm 中,右键 ...
- django中多个app放入同一文件apps
新建一个apps文件夹 需要整理的app文件夹拖到同一个文件夹中,即apps.(弹出对话框,取消勾选Search for references) 在pycharm中,右键apps文件夹--选择mark ...
- python 将指定文件夹中的指定文件放入指定文件夹中
import os import shutil import re #获取指定文件中文件名 def get_filename(filetype): name =[] final_name_list = ...
随机推荐
- nodejs 实现 磁力链接资源搜索 BT磁力链接爬虫
项目简介 前端站点 项目效果预览 http://findcl.com 使用 nodejs 实现磁力链接爬虫 磁力链接解析成 torrent种子信息,保存到数据库,利用 Elasticsearch 实现 ...
- Android CheckBox的监听事件
1.在xml文件中定义CheckBox,一定要定义id <CheckBox android:id="@+id/beijing" android:layout_width=&q ...
- ccf201912-1 报数 C++代码实现
代码实现: #include<iostream> using namespace std; /*题目限制为三位数*/ /*思路: 1.用一个长度为4的数组(初值为0)保存每个人分别跳过了几 ...
- 使用Vue_CLI_3快速创建项目
- 【SpringBoot实战】数据访问
前言 在开发中我们通常会对数据库的数据进行操作,SpringBoot对关系性和非关系型数据库的访问操作都提供了非常好的整合支持.SpringData是spring提供的一个用于简化数据库访问.支持云服 ...
- Linux---必备命令(2)
进程相关命令 # 查看系统所有的进程 ps -ef ps -ef | grep vim # 过滤出vim有关的进程 ps -ef | grep vim # 过滤出22端口的信息 ps -tunlp | ...
- 3.Docker容器学习之新手基础使用
原文地址: http://blog.weiyigeek.top/2019/5/2-docker%E5%AD%A6%E4%B9%A0%E4%B9%8B%E5%9F%BA%E7%A1%80%E4%BD%B ...
- 【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家
前言 因为前段时间新项目已经完成目前趋于稳定,所以最近我被分配到了公司的运维组,负责维护另外一个项目,包含处理客户反馈的日常问题,以及对系统缺陷进行优化. 经过了接近两周的维护,除了日常问题以外,代码 ...
- Sliding Window - 题解【单调队列】
题面: An array of size n ≤ 106 is given to you. There is a sliding window of size k which is moving fr ...
- XCTF练习题---MISC---ext3
XCTF练习题---MISC---ext3 flag:flag{sajbcibzskjjcnbhsbvcjbjszcszbkzj} 解题步骤: 1.下载附件,观察题目,发现题目跟Linux有关,我们换 ...