C++模板类分离式编译


关于C++模板分离式编译

分离编译模式

一个项目如果有多个源文件组成,每个源文件单独编译,形成目标文件。最后通过链接器将所有的目标文件链接起来,形成一个可执行的文件。这个过程就叫做分离编译。

分离式编译的优势

如果工程巨大(比如linux内核源码) , 当你只修改了其中一个文件的时候,只要编译改动过的就可以了,不需要全部重新编译。

模板不能分离编译

有一个项目,其中函数声明放在”test.h”中,函数实现放在”test.cpp”中,函数调用放在”main.cpp”中。如果没用使用模板,将不会有任何问题。而且这样是被推荐使用的。但是如果有函数的实现是模板函数或有模板类,将会出现链接错误。代码如下:

//在头文件fun.h中
#include
template
class A {
public:
void func(T t);
};

//在源文件fun.cpp中
#include
#include"fun.h"
template
void A::func(T t)
{
std::cout << t << std::endl;
}

//在main.cpp中
#include
#include"fun.h"
int main(void)
{
A k;
k.func(2);
system("pause");
return 0;
}


方法一
现在我才想起来以前写stl的时候,出现这个错误,死活想不到是什么原因。

来看一下我在前段时间写stl时的一种正确的处理方法

//在头文件fun.h中
#include
template
class A {
public:
void func(T t);
};

//在头文件fun.impl.h中
#include
template
void A::func(T t)
{
std::cout << t << std::endl;
}

//在main.cpp中
#include
#include"fun.h" //1
#include"fun.impl.h"//2
//1和2都必须包含,否则会报错
int main(void)
{
A k;
k.func(2);
system("pause");
return 0;
}

这种方法可以通过编译,其实原理很简单,在C++中类模板不支持分离式编译,即我们必须把类模板的声明和定义写在.h文件中,因为头文件的数据相当于全局变量,所以其实就是相当于在一个头文件中既声明又定义,如果写在两个头文件中,方便我们编写,但实质还是在头文件中同时声明和定义。

方法二
//在头文件fun.h中
#include
template
class A {
public:
void func(T t);
};

//在源文件fun.cpp中
#include
#include"fun.h"
template
void A::func(T t)
{
std::cout << t << std::endl;
}

//在main.cpp中
#include
#include"fun.cpp"
int main(void)
{
A k;
k.func(2);
system("pause");
return 0;
}

这种解决方法直接包含了实现文件,让编译器直接找到定义和实现,这样的效果跟把定义和实现都放在fun.h里是一样的。所以原理还是和方法一一样。

为什么C++C++编译器不能支持对模板的分离式编译
C++是分别单独编译,对于每个cpp文件,预编译为编译单元,编译的时候不需要其他文件,可生成obj文件,然后链接成exe文件。当.h中声明了一个方法模板,.cpp中定义一个方法模板,在main函数中包含.h文件,然后使用模板方法。单独编译.cpp,并不会生成特定版本的方法,因为C++规定,模版只有在使用的时候,才实例化一个方法。在main.cpp中,相当于声明了模版方法,但是不知道模版方法的定义,因此也没法实例化模版方法,只能希望连接器在其他的obj文件中,找到对应的实例化方法。但是在连接的时候,找不到对应的方法,连接错误。

上面问题的关键是:C++单独,分别编译。Fun.cpp定义了模版方法,但是没有使用,因此不会实例化模版方法。而在main.cpp中,使用了模版方法,但是不知道模版方法的定义,也没办法实例化方法,只能寄希望于连接的时候,从其他obj中找到,当然找不到,就出错了。
————————————————


 

为什么模板类与模板成员函数不能分文件写(.h与.cpp)

oliver-11于 2019-05-29 17:21:04 发布1472头文件中进行类声明,在cpp文件中实现,但使用模板时应注意目前的C++编译器还无法分离编译,最好将实现代码和声明代码均放在头文件中。如:

  1.    
  2.   test.h
  3.    
  4.   template <class T>
  5.   class CTest
  6.   {
  7.   public:
  8.   T& GetValue();
  9.   void SetValue(const T& _Value);
  10.   protected:
  11.   T m_Value;
  12.   };
  13.    
  14.   test.cpp
  15.    
  16.   template <class T>
  17.   T& CTest::GetValue()
  18.   {
  19.   return m_Value;
  20.   }
  21.    
  22.   template<class T>
  23.   void CTest::SetValue(const T& _Value)
  24.   {
  25.   m_Value = _Value;
  26.   }



在这儿test.cpp中的内容应放在test.h中,否则在生成最终可执行程序时就会出现错误(在链接时会出错)。因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。

《C++编程思想》第15章(第300页)说明了原因:
模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

 技巧15:模板定义的位置在哪里?是.cpp文件吗?

 通常情况下,你会在.h文件中声明函数和类,而将它们的定义放置在一个单独的.cpp文件中。但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。这就是为什么所有的STL头文件都包含模板定义的原因。

 另外一个方法就是使用关键字“export”!你可以在.h文件中,声明模板类和模板函数;在.cpp文件中,使用关键字export来定义具体的模板类对象和模板函数;然后在其他用户代码文件中,包含声明头文件后,就可以使用该这些对象和函数了。例如:

  1.   // output.h - 声明头文件
  2.   template<class T> void output (const T& t);
  3.    
  4.   // out.cpp - 定义代码文件
  5.   #include <****>
  6.   export template<class T> void output (const T& t) {std::cerr << t;}
  7.    
  8.   //main.cpp:用户代码文件
  9.    
  10.   #include "output.h"
  11.   void main() // 使用output()
  12.   {
  13.   output(4);
  14.   output("Hello");
  15.   }

某种程度上,这有点类似于为了访问其他编译单元(如另一代码文件)中普通类型的变量或对象而采用的关键字extern。
  但是,这里还有一个不得不说的问题:并非所有的编译器都支持export关键字(我们最熟悉、最常用的两款编译器VS 和 GCC就是不支持export的典型代表)。对于这种不确定,最好的方法就是采用解决方案一:声明定义放在一起,虽然这在某种程度上破坏了C++编程的优雅性。

 分离编译模式(Separate Compilation Model)允许在一处翻译单元(Translation Unit)中定义(define)函数、类型、类对象等,在另一处翻译单元引用它们。编译器(Compiler)处理完所有翻译单元后,链接器(Linker)接下来处理所有指向 extern 符号的引用,从而生成单一可执行文件。该模式使得 C++ 代码编写得称心而优雅。

 然而该模式却驯不服模板(Template)。标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。

  以优雅著称的 C++ 是不能容忍有此“败家玩意儿”好好活着的。标准 C++ 为此制定了“模板分离编译模式(Separation Model)”及 export 关键字。然而由于 template 语义本身的特殊性使得 export 在表现的时候性能很次。编译器不得不像 .net 和 java 所做的那样,为模板实体生成一个“中间伪代码(IPC,intermediate pseudo - code)”,使得其它翻译单元在实例化时可找到定义体;而在遇到实例化时,根据指定的 typename 实参再将此 IPC 重新编译一遍,从而达到“分离编译”的目的。因此,该标准受到了几乎所有知名编译器供应商的强烈抵制。
  谁支持 export 呢?Comeau C/C++ 和 Intel 7.x 编译器支持。而以“百分百支持 ISO ”著称的 VS 和 GCC 却对此视而不见。真不知道这两大编译器“百分百支持”的是哪个版本的 ISO。在 VS 2008 中,export 关键字在 IDE 中被标蓝,表示 VS IDE 认识它,而编译时,会用警告友情提示你“不支持该关键字”,而配套的 MSDN 9 中的 C++ keywords 页则根本查不到该关键字;而在 VS 2010 中,就没那么客气了,尽管 IDE 中仍然会将之标蓝,但却会直截了当地报错。C++11已经取消使用关键字export。


为何C++编译器不能支持对模板的分离式编译

时间  2019-11-13 标签 为何 c++ 编译器 不能 支持  模板 分离  编译 栏目 C&C++ 繁體版 原文   https://www.cnblogs.com/huolong-blog/p/7600135.html

今天在写队列模板类的时候,我把模板类的声明与实现分开写后,在测试的.cpp文件编译时报错“未定义的引用....."c++

在网上查阅得知缘由是:c++编译器不支持对模板的分离式编译windows

首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的全部.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,而后编译器编译该.cpp文件为一个.obj文件(假定咱们的平台是win32),后者拥有PE(Portable Executable,即windows可执行文件)文件格式,而且自己包含的就已是二进制码,可是不必定可以执行,由于并不保证其中必定有main函数。当编译器将一个工程里的全部.cpp文件以分离的方式编译完毕后,再由链接器(linker)进行链接成为一个.exe文件。函数

举个例子:测试

//---------------test.h-------------------//.net

void f();//这里声明一个函数fblog

//---------------test.cpp--------------//队列

#include”test.h”编译器

void f()it

{io

…//do something

}  //这里实现出test.h中声明的f函数

//---------------main.cpp--------------//

#include”test.h”

int main()

f(); //调用f,f具备外部链接类型

在这个例子中,test. cpp和main.cpp各自被编译成不一样的.obj文件(姑且命名为test.obj和main.obj),在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,因此,编译器将这里的f看做外部链接类型,即认为它的函数实现代码在另外一个.obj文件中,本例也就是test.obj,也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在main.obj中对f的调用只会生成一行call指令,像这样:

call f [C++中这个名字固然是通过mangling[处理]过的]

在编译时,这个call指令显然是错误的,由于main.obj中并没有一行f的实现代码。那怎么办呢?这就是链接器的任务,链接器负责在其它的.obj中(本例为test.obj)寻找f的实现代码,找到之后将call f这个指令的调用地址换成实际的f的函数进入点地址。须要注意的是:链接器实际上将工程里的.obj“链接”成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部链接符号在另外一个.obj中的地址,而后替换原来的“虚假”地址。

这个过程若是说的更深刻就是:

call f这行指令其实并非这样的,它其实是所谓的stub,也就是一个jmp 0xABCDEF。这个地址多是任意的,然而关键是这个地址上有一行指令来进行真正的call f动做。也就是说,这个.obj文件里面全部对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样作的好处就是链接器修改地址时只要对后者的call XXX地址做改动就好了。可是,链接器是如何找到f的实际地址的呢(在本例中这处于test.obj中),由于.obj与.exe的格式是同样的,在这样的文件中有一个符号导入表和符号导出表(import table和export table)其中将全部符号和它们的地址关联起来。这样链接器只要在test.obj的符号导出表中寻找符号f(固然C++对f做了mangling)的地址就好了,而后做一些偏移量处理后(由于是将两个.obj文件合并,固然地址会有必定的偏移,这个链接器清楚)写入main.obj中的符号导入表中f所占有的那一项便可。

这就是大概的过程。其中关键就是:

编译main.cpp时,编译器不知道f的实现,因此当碰到对它的调用时只是给出一个指示,指示链接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。

编译test.cpp时,编译器找到了f的实现。因而乎f的实现(二进制代码)出如今test.obj里。

链接时,链接器在test.obj中找到f的实现代码(二进制)的地址(经过符号导出表)。而后将main.obj中悬而未决的call XXX地址改为f实际的地址。完成。

然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“实例化”的过程。举个例子:

//----------main.cpp------//

template

void f(T t)

{}

int main()

…//do something

f(10); // call f 编译器在这里决定给f一个f的实例

…//do other thing

也就是说,若是你在main.cpp文件中没有调用过f,f也就得不到实例化,从而main.obj中也就没有关于f的任意一行二进制代码!若是你这样调用了:

f(10); // f得以实例化出来

f(10.0); // f得以实例化出来

这样main.obj中也就有了f,f两个函数的二进制代码段。以此类推。

然而实例化要求编译器知道模板的定义,不是吗?

看下面的例子(将模板的声明和实现分离):

//-------------test.h----------------//

template

class A

public:

void f(); // 这里只是个声明

};

//---------------test.cpp-------------//

#include”test.h”

template

void A::f()  // 模板的实现

   …//do something

//---------------main.cpp---------------//

#include”test.h”

int main()

A a;

f(); // #1

编译器在#1处并不知道A::f的定义,由于它不在test.h里面,因而编译器只好寄但愿于链接器,但愿它可以在其余.obj里面找到A::f的实例,在本例中就是test.obj,然而,后者中真有A::f的二进制代码吗?NO!!!由于C++标准明确表示,当一个模板不被用到的时侯它就不应被实例化出来,test.cpp中用到了A::f了吗?没有!!因此实际上test.cpp编译出来的test.obj文件中关于A::f一行二进制代码也没有,因而链接器就傻眼了,只好给出一个链接错误。可是,若是在test.cpp中写一个函数,其中调用A::f,则编译器会将其实例化出来,由于在这个点上(test.cpp中),编译器知道模板的定义,因此可以实例化,因而,test.obj的符号导出表中就有了A::f这个符号的地址,因而链接器就可以完成任务。

关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另外一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄但愿于链接器)。这种模式在没有模板的状况下运行良好,但遇到模板时就傻眼了,由于模板仅在须要的时候才会实例化出来,因此,当编译器只看到模板的声明时,它不能实例化该模板,只能建立一个具备外部链接的符号并期待链接器可以将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,因此,整个工程的.obj中就找不到一行模板实例的二进制代码,因而链接器也黔驴技穷了。