C++模板不支持分离编译的思考

前言

在我初入程序员这行时,因为学生阶段只写一些简单的考试题,所以经常是将声明和实现统一写到一个文件中,导致同事在用我的代码时一脸懵逼,因此还有一段悲惨的往事。

为什么代码需要分离编译

通常来说,在写C/C++代码是,一般是将函数的声明放在.h文件中,而函数的定义放在.c/.cpp文件中,然后将.h文件包含在.cpp文件中。

首先,我们来看看什么是文件包含。

文件包含

在写第一个程序的时候,我们都会写下诸如这种包含:

#include <stdio.h>

这就是一种文件包含,意为将stdio.h中的所有声明在预处理阶段全部拷贝到本文件中。


分离编译的原理

从编译的角度来说,声明文件在编译的时候不会为其分配空间,而在编译定义文件,即.c/.cpp文件时,编译器为每个函数,全局变量分配内存空间。

在程序的预编译、编译和汇编阶段,如果程序中调用了某个函数,需要这个函数的声明或者实现,如果仅有函数的声明而没有实现,编译器就会在目标文件中做一个标记,告诉链接器在链接的时候需要去找这个函数的声明,把问题移交给链接器,如果链接器找不到,就会报错:undefined reference。

从这个原理出发,一个.h文件可以被多个文件包含,前提是这个.h文件没有定义行为,如果有类似于

int val=0;
或者
void func(){}

这种定义行为,当这个.h文件在被两个及以上文件包含时,在编译时就会出现val或者func()被两个文件同时定义,链接器在对目标文件进行链接的时候就会发现同时有两个val或func(),就是重复定义行为,而声明不会分配内存空间,即不会存在于目标文件中,多个声明并不会影响编译过程。

同时,将声明和定义分离编译有利于程序的移植和复用。



C++模板的分离编译问题

如果说C++实现泛型编程的方法是什么,熟悉C++的人都会脱口而出:模板。

是的,利用C++模板的特性,可以轻松写出高移植性,高复用性、好维护的代码,但是在这里,我要说的是模板的缺点,C++的模板并不支持分离编译。

首先,给出一个示例:

定义三个文件:test.cpp,test.h,main.cpp:

test.h:

#include <iostream>
using namespace std;
template<class T1,class T2>
void func(T1 t1,T2 t2);

test.cpp:

#include "test.h"
using namespace std;
template<class T1,class T2>
void func(T1 t1,T2 t2)
{
cout<<t1<<t2<<endl;
}

main.cpp:

#include "test.h"
int main()
{
string str("downey");
int i=5;
func(str,5);
}

标准的分离编译模式:在test.h中声明func(),在test.cpp中定义func()实现,在main.cpp中包含test.h头文件并应用func().

键入编译命令:

g++ test.cpp test.h main.cpp -o test

结果却是:

/tmp/ccqLWRwf.o: In function `main':
main.cpp:(.text+0x6c): undefined reference to `void func<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)'
collect2: error: ld returned 1 exit status

非常熟悉的undefined reference to错误。

那么,既然我已经定义了func()函数,为什么还会报错呢。

模板的编译模式

让我们从编译器的角度出发来观察模板编译的问题,做一些假设:

假设1:如果我只用test.cpp和test.h文件来编译出目标文件会发生什么?

首先,这种方式是可行的,用g++提供的-C参数对目标文件只编译不链接:

g++ -C test.cpp test.h -o test.o

生成的test.o文件,用linux下的 nm 命令查看目标文件符号表:

(关于nm命令的应用可以查看我的另一篇博文)

nm -C -n test.o

结果是:

U __cxa_atexit
U __dso_handle
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 t __static_initialization_and_destruction_0(int, int)
0000000000000000 b std::__ioinit
000000000000003e t _GLOBAL__sub_I_test.cpp

丝毫看不到func()的影子,也就是说编译出的二进制文件中根本没有为func()分配内存空间,这是为什么呢?

答案是:模板只有在使用的时候才进行实例化

通俗地说,用户写一个模板定义,模板本身提供任何数据类型的支持,而编译器在编译出的目标文件中只支持确定的类型例如func(int,int),func(string char),而不能支持func(模板类,模板类...)(注意模板是C++的特性而非编译器的特性)。

那么编译器在编译的时候根本不知道用户要传入什么样的参数,所以无法确定模板的实例,所以编译器只能等到用户使用此模板的时候才能进行实例化,才能确定模板的具体类型,从而为其分配内存空间,像上述例子中,模板没有实例化,编译器也就不会为模板分配内存空间。

那为什么在同时编译test.cpp test.h main.cpp的例子中,模板在main函数中有进行实例化,还是会提示无定义行为呢?

事实上编译器是对所有的.cpp文件分开编译的:

main.cpp依赖test.h,就会将main.cpp和test.h编译成main.o目标文件

test.cpp依赖test.h,将test.cpp和test.h编译成test.o目标文件

然后链接器将main.o和test.o以及一些标准库链接成可执行文件。

但是由上的分析得知,在编译test.o文件过程中编译器并没有将func()实例化,也就是没有func()的定义,所以在链接的时候出现未定义行为。

那如果我在.cpp文件中将模板实例化呢?

在以上的示例中,我在test.cpp中添加一个func()的调用:

void func1(string str,int val)

{

func(str,val)

}

即test.cpp变成:

#include "test.h"

template<class T1,class T2>
void func(T1 t1,T2 t2)
{
cout<<t1<<t2<<endl;
} void func1(string str,int val)
{
func(str,val); //将func实例化
}

main.cpp保持不变:

#include "test.h"
int main()
{
string str("downey");
int i=5;
func(str,5);
}

这时候再进行编译:

g++ test.cpp test.h main.cpp -o test

竟然可以编译通过,而且键入命令

./test

可以正常运行,这个例子说明只要我在定义文件中进行了实例化,编译器就会为这个模板分配内存空间。

编译器是否能完全地支持这个模板函数呢?

在上述例子中需要注意到的是,在模板定义文件中使用的是func(str,int),在main.cpp中使用的也是func(str,int),那在main.cpp中是否也能支持模板的其他重载函数呢?例如func(string,char)或者func(string,string)。

对此,需要修改main函数为:

  #include "test.h"
int main()
{
string str("downey");
string str_tmp("1234567");
func(str,str_tmp);
}

结果是:

/tmp/ccBe2hDd.o: In function `main':
main.cpp:(.text+0xc2): undefined reference to `void func<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: error: ld returned 1 exit status

结果很显然,无定义行为。

对此,我们再用nm指令查看目标文件test.o中的符号表:

0000000000000000 W void func<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)
0000000000000000 T func1(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)
0000000000000000 b std::__ioinit
0000000000000087 t __static_initialization_and_destruction_0(int, int)
00000000000000c5 t _GLOBAL__sub_I__Z5func1NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEi

发现符号表中有func(string,int)和func1()函数,却并没有func(string,string)的函数存在,所以,调用func(string,string)将会出现未定义行为,如果需要用到func(string,string),就得在test.cpp文件中将func(string,string)实例化一次,也就是使用一次。

模板编译问题的解决方案

注:虽然这里以函数模板为例,但应用于类模板是一样的

解决方案1:提前实例化模板

看到这里,各位观众们应该大概能想到一种分离编译的解决方案了,在模板定义文件中将所有要用到的模板函数都进行实例化,不过说实话,这很扯蛋,而且完全不符合程序的美学。

设计泛型接口本身就是为了多态,并不需要知道调用者以什么方式调用,这样实现的话接口设计者就得知道调用者的所有调用方式!但事实上是确实可以这么做。

解决方案2:定义实现全部放在同一个.h文件中

第二种解决方案就是将模板的定义和实现全部放到一个.h文件中,为什么这样又可以呢?

当某个.cpp文件用到某个模板时,包含了相应的头文件,而头文件中同时由模板的定义和声明,在cpp文件中使用就相当于对这个模板进行了实例化(.cpp文件依赖.h文件编译成.o文件,cpp文件中实例化,h文件中进行定义和声明),这样就可以使用模板了。

这也是常见的做法,STL就是这样实现的,遗憾的是,这违背了分离编译的思想。

好了,关于C++模板分离编译的问题就到此为止了,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.

(完)

关于C++模板不能分离编译的问题思考的更多相关文章

  1. c++ 模板 不能 分离编译

    C++Template头文件和定义分开编译的问题 (1) // Foo.htemplate<typename T>class Foo{public:void f();}; // Foo.c ...

  2. 【C++】模板简述(四):模板为什么不支持分离编译?

    上文简述了类模板相关功能,本文主要简述为什么模板不支持分离编译? 在C++中,为了一个项目的规范,我们通常把代码归为三类:声明文件.实现文件.测试文件. 比如,我要用C++实现一个链表,那么就会创建这 ...

  3. c++模板函数声明定义分离编译错误详解

    今天看到accelerated c++上有个简单的vector容器的实现Vec,就再vs2008上编译了下: ///// Vec.h #ifndef GUARD_VEC_H #define GUARD ...

  4. C++ —— 类模板的分离式编译

    目录 对于C++中类模板的分离式编译的认识 具体的实例 1.对于C++中类模板的分离式编译的认识 为什么C++编译器不能支持对模板的分离式编译(博文链接) 主要内容:编译器编译的一般工作原理.对模版的 ...

  5. 【转】为什么C++编译器不能支持对模板的分离式编译

    出处:刘未鹏(pongba) http://blog.csdn.net/pongba)   首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h ...

  6. 为什么C++编译器不能支持对模板的分离式编译

    首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个 ...

  7. html 模板 swig 预编译插件 grunt-swig-precompile

    GitHub grunt-swig-precompile NPM grunt-swig-precompile 在书写前端静态页面的时候,每个页面总在书写很多重复的标签. 为了提高效率,结合 swig. ...

  8. 单项目实现vendor分离编译,增加编译效率(vue-cli)

    1.在build文件夹下添加文件:webpack.dll.config.js const path = require('path') const webpack = require('webpack ...

  9. 基于前端js模板替换的多语言方案思考

    最近在做将一个系统多语言化的项目,系统使用的是ASP.NET,直接使用了一种已有的方案:在页面渲染时采用正则表达式替换{XXX:001 确定}格式的标记.但是这个方式增加了服务端的字符串处理,对页面性 ...

随机推荐

  1. Spring cloud微服务安全实战-6-10sentinel之热点和系统规则

    热点规则 热点就是经常访问的数据.很多时候我们希望争对某一些热点数据,然后来进行限制.比如说商品的信息这个服务,我们给它做一个限流,qps是100,某一天我想做一个秒杀活动,可能会有很大的流量,这个时 ...

  2. PAT 甲级 1080 Graduate Admission (30 分) (简单,结构体排序模拟)

    1080 Graduate Admission (30 分)   It is said that in 2011, there are about 100 graduate schools ready ...

  3. 【翻译】Flink Table Api & SQL — 自定义 Source & Sink

    本文翻译自官网: User-defined Sources & Sinks  https://ci.apache.org/projects/flink/flink-docs-release-1 ...

  4. NI MAX中缺少串口(转)

    Software Measurement & Automation Explorer (MAX) Driver NI-VISA     问题详述 在NI MAX中,设备和接口中的串口不可用或缺 ...

  5. Vue 引入 .md 文件,解析markdown语法

    module.exports = { chainWebpack: config => { config.module .rule('md') .test(/\.md$/) .use('html- ...

  6. MyBatis插入记录时返回主键id的方法

    有时候插入记录之后需要使用到插入记录的主键,通常是再查询一次来获取主键,但是MyBatis插入记录时可以设置成返回主键id,简化操作,方法大致有两种. 对应实体类: public class User ...

  7. [LeetCode] 304. Range Sum Query 2D - Immutable 二维区域和检索 - 不可变

    Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper lef ...

  8. [LeetCode] 543. Diameter of Binary Tree 二叉树的直径

    Given a binary tree, you need to compute the length of the diameter of the tree. The diameter of a b ...

  9. OpenJudge 4152 最佳加法表达式

    总时间限制: 1000ms 内存限制: 65536kB 描述 给定n个1到9的数字,要求在数字之间摆放m个加号(加号两边必须有数字),使得所得到的加法表达式的值最小,并输出该值.例如,在1234中摆放 ...

  10. GAN代码实战

    batch normalization 1.BN算法,一般用在全连接或卷积神经网络中.可以增强整个神经网络的识别准确率以及增强模型训练过程中的收敛能力2.对于两套权重参数,例如(w1:0.01,w2: ...