提问



来自C ++标准库的引用:教程和手册:[52]



  目前使用模板的唯一可移植方法是使用内联函数在头文件中实现它们。



为什么是这样?


(澄清:头文件不是唯一的便携式解决方案。但它们是最方便的便携式解决方案。)

最佳参考


将实现放在头文件中是 not ,请参阅本答案末尾的替代解决方案。


无论如何,代码失败的原因是,在实例化模板时,编译器会创建一个具有给定模板参数的新类。例如:


template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 


在读取这一行时,编译器将创建一个新类(让我们称之为FooInt),这相当于以下内容:


struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}


因此,编译器需要访问方法的实现,使用模板参数实例化它们(在本例中为int)。如果这些实现不在标题中,则它们将不可访问,因此编译器将无法实例化模板。


一个常见的解决方案是在头文件中编写模板声明,然后在实现文件(例如.tpp)中实现该类,并在头的末尾包含此实现文件。


// Foo.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

// Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


这样,实现仍然与声明分离,但编译器可以访问。


另一个解决方案是保持实现分离,并显式实例化您需要的所有模板实例:


// Foo.h

// no implementation
template <typename T> struct Foo { ... };

//----------------------------------------    
// Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float


如果我的解释不够清楚,你可以看一下关于这个主题的C ++ Super-FAQ。[53]

其它参考1


这里有很多正确答案,但我想补充一下(为了完整性):


如果您在实现cpp文件的底部对模板将使用的所有类型进行显式实例化,则链接器将能够像往常一样找到它们。


编辑:添加显式模板实例化的示例。在定义模板后使用,并且已定义所有成员函数。


template class vector<int>;


这将实例化(并因此使链接器可用)类及其所有成员函数(仅)。类似的语法适用于模板函数,因此如果您有非成员运算符重载,则可能需要对这些函数执行相同操作。


上面的例子是相当无用的,因为vector在头文件中是完全定义的,除非公共包含文件(预编译头文件?)使用extern template class vector<int>,以防止它在所有其他中实例化它(1000?)使用矢量的文件。

其它参考2


这是因为需要单独编译,因为模板是实例化风格的多态。


让我们更接近具体的解释。说我有以下文件:



  • foo.h中

    • 声明class MyClass<T>
    • 的界面

  • Foo.cpp中

    • 定义class MyClass<T>
    • 的实现

  • bar.cpp

    • 使用MyClass<int>




单独的编译意味着我应该能够独立于 bar.cpp 编译 foo.cpp 。编译器完全独立地在每个编译单元上完成分析,优化和代码生成的所有艰苦工作;我们不需要进行整个程序分析。只有链接器需要立即处理整个程序,并且链接器的工作要容易得多。


当我编译 foo.cpp 时, bar.cpp 甚至不需要存在,但我仍然可以链接 foo.o 我已经和我刚刚制作的 bar.o 在一起,无需重新编译 foo.cpp foo.cpp 甚至可以编译成动态库,在没有 foo.cpp 的情况下分布在其他地方,并与我写 foo.cpp后多年编写的代码链接即可。


实例化样式多态意味着模板MyClass<T>并不是一个通用类,可以编译为可以适用于T的任何值的代码。这会增加开销,例如装箱,需要将函数指针传递给分配器和构造函数等.C ++模板的目的是避免编写几乎相同的class MyClass_intclass MyClass_float等,但仍然能够以编译的代码结束大多数情况下,我们已经分别编写了每个版本。所以模板字面上一个模板;一个类模板不一个类,它为我们遇到的每个T创建一个新类的秘诀。模板不能编译成代码,只能编译实例化模板的结果。


因此,当编译 foo.cpp 时,编译器无法看到 bar.cpp 知道需要MyClass<int>。它可以看到模板MyClass<T>]],但它不能发出代码(它是一个模板,而不是一个类)。当编译 bar.cpp 时,编译器可以看到它需要创建一个[[]] 28]],但它看不到模板MyClass<T>(只有 foo.h 中的界面)所以它无法创建它。


如果 foo.cpp 本身使用MyClass<int>,那么在编译 foo.cpp 时会生成代码,所以当 bar.o 链接到 foo.o ,它们可以连接起来并且可以正常工作。我们可以使用这个事实来允许通过编写单个模板在.cpp文件中实现一组有限的模板实例化。但 bar.cpp 无法将模板用作模板并在其喜欢的任何类型上实例化它;它只能使用预先存在的版本 foo.cpp 的作者认为提供的模板类。


您可能认为在编译模板时,编译器应该生成所有版本,并且在链接期间过滤掉从未使用过的版本。除了巨大的开销和这种方法将面临的极端困难之外,因为类型修饰符功能(如指针和数组)甚至只允许内置类型产生无数类型,当我现在扩展程序时会发生什么通过增加:



  • baz.cpp

    • 声明并实现class BazPrivate,并使用MyClass<BazPrivate>




除非我们要么,否则没有办法可行



  1. 每次我们更改程序中的任何其他文件时,必须重新编译 foo.cpp ,以防它添加了MyClass<T>的新小说实例

  2. 要求 baz.cpp 包含(可能通过标头包含)MyClass<T>的完整模板,以便编译器在编译 baz时可以生成MyClass<BazPrivate>的.cpp 即可。



没有人喜欢(1),因为整个程序分析编译系统需要永远进行编译,因为它使得在没有源代码的情况下分发编译库变得不可能。所以我们改为(2)。

其它参考3


在将模板实际编译为目标代码之前,编译器需要实例化模板。只有在模板参数已知的情况下才能实现此实例化。现在想象一下在a.h中声明模板函数的场景,在a.cpp中定义并在b.cpp中使用。编译a.cpp时,不一定知道即将进行的编译b.cpp将需要模板的实例,更不用说具体的实例。对于更多的头文件和源文件,情况可能会变得更加复杂。


有人可以说,编译器可以变得更聪明,可以展望模板的所有用途,但我确信创建递归或其他复杂场景并不困难。 AFAIK,编译器不做这样的预测。正如Anton所指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(但是?)。

其它参考4


实际上,C ++ 11之前的C ++标准版本定义了export关键字, 可以简单地在头文件中声明模板并在其他地方实现它们。


不幸的是,没有一个流行的编译器实现了这个关键字。我所知道的唯一一个是由Edison Design Group编写的前端,由Comeau C ++编译器使用。所有其他人坚持你在头文件中编写模板,需要定义代码以进行适当的实例化(正如其他人已经指出的那样)。


因此,ISO C ++标准委员会决定删除以C ++ 11开头的模板export功能。

其它参考5


虽然标准C ++没有这样的要求,但是一些编译器要求所有函数和类模板都需要在它们使用的每个转换单元中可用。实际上,对于那些编译器,模板函数的主体必须在头文件中可用。重复:这意味着那些编译器不允许在非头文件中定义它们,例如.cpp文件


有一个 export 关键字可以缓解这个问题,但它无法移植到最近。

其它参考6


模板必须在头文件中使用,因为编译器需要实例化不同版本的代码,具体取决于模板参数的给定/推导参数。请记住,模板不直接代表代码,而是代码的多个版本的模板。
.cpp文件中编译非模板函数时,您正在编译具体的函数/类。模板不是这种情况,可以用不同类型实例化,即在用具体类型替换模板参数时必须发出具体代码。


export关键字有一个功能,用于单独编译。
export功能在C++11和AFAIK中不推荐使用,只有一个编译器实现了它。你不应该使用export。在C++C++11中不可能单独编译,但也许在C++17中,如果概念适用,我们可以有一些方法单独编译。


要实现单独的编译,必须单独进行模板体检查。似乎可以通过概念来解决问题。看看最近提交的这篇论文
标准委员会会议。我认为这不是唯一的要求,因为您仍然需要在用户代码中实例化模板代码的代码。 [54]


模板的单独编译问题我认为这也是迁移到模块时出现的问题,目前正在进行中。

其它参考7


这意味着定义模板类的方法实现的最便携方式是在模板类定义中定义它们。


template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

其它参考8


即使上面有很多好的解释,我也错过了将模板分成标题和正文的实用方法
我主要担心的是当我更改其定义时,避免重新编译所有模板用户
模板体中的所有模板实例化对我来说都不是一个可行的解决方案,因为模板作者可能不知道它的用法和模板用户是否有权修改它。

我采用了以下方法,该方法也适用于较旧的编译器(gcc 4.3.4,aCC A.03.13)。


对于每个模板的使用,在它自己的头文件中都有一个typedef(从UML模型生成)。它的主体包含实例化(最终在最后链接的库中)。

模板的每个用户都包含该头文件并使用typedef。


示意图:


MyTemplate.h:


#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif


MyTemplate.cpp:


#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}


MyInstantiatedTemplate.h:


#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif


MyInstantiatedTemplate.cpp:


#include "MyTemplate.cpp"

template class MyTemplate< int >;


main.cpp中:


#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}


这样,只需要重新编译模板实例化,而不是所有模板用户(和依赖项)。

其它参考9


这是完全正确的,因为编译器必须知道它的分配类型。所以模板类,函数,枚举等也必须在头文件中实现,如果它要公开或者是库的一部分(静态或动态),因为头文件的编译不像c/cpp文件那样是。如果编译器不知道类型是不能编译它。在.Net中它可以因为所有对象都派生自Object类。这不是.Net。

其它参考10


如果关注的是额外的编译时间和二进制大小膨胀是通过将.h编译为使用它的所有.cpp模块的一部分而产生的,在许多情况下,你可以做的是使模板类从非模板化的基类下降接口的非类型相关部分,该基类可以在.cpp文件中实现它。

其它参考11


单独实现的方法如下。


//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>


inner_foo具有前向声明。 foo.tpp有实现并包含inner_foo.h;和foo.h只有一行,包括foo.tpp。


在编译时,将foo.h的内容复制到foo.tpp,然后将整个文件复制到foo.h,然后编译。这样,没有限制,命名是一致的,以换取一个额外的文件。


我这样做是因为代码的静态分析器在看不到* .tpp中类的前向声明时会中断。在任何IDE中编写代码或使用YouCompleteMe或其他代码时,这很烦人。

其它参考12


在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。
在编译和链接过程中.cpp文件被转换为纯对象或机器代码,其中包含引用或未定义的符号,因为main.cpp中包含的.h文件没有实现YET。这些已准备好与另一个定义模板实现的目标文件链接,因此您有一个完整的a.out可执行文件。
但是,由于模板需要在编译步骤中处理,以便为您在主程序中执行的每个模板实例化生成代码,因此链接不会有帮助,因为将main.cpp编译为main.o然后编译模板.cpp到template.o然后链接不会实现模板的目的,因为我将不同的模板实例链接到相同的模板实现!模板应该做相反的事实,即有一个实现但允许许多可用的实例化通过使用一节课。


意义typename T在编译步骤中取代了s而不是链接步骤,所以如果我尝试编译模板而不将T替换为具体的值类型,那么它就不会工作,因为那是模板的定义是编译时进程,而btw元编程就是使用这个定义。

其它参考13


只是为了在这里添加值得注意的东西。当它们不是函数模板时,可以在实现文件中定义模板类的方法。





myQueue.hpp:


template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    





myQueue.cpp:


// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}