为什么模板只能在头文件中实现?

Mai*_*nID 1660 c++ templates c++-faq

引自C++标准库:教程和手册:

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

为什么是这样?

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

Luc*_*lle 1453

它是不是需要把执行的头文件,看到这个答案的末尾替代解决方案.

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

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

// somewhere in a .cpp
Foo<int> f; 
Run Code Online (Sandbox Code Playgroud)

读取此行时,编译器将创建一个新类(让我们调用它FooInt),这相当于以下内容:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}
Run Code Online (Sandbox Code Playgroud)

因此,编译器需要访问方法的实现,以使用模板参数(在本例中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
}
Run Code Online (Sandbox Code Playgroud)

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

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

// 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
Run Code Online (Sandbox Code Playgroud)

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

  • 实际上,显式实例化需要在.cpp文件中,该文件可以访问所有Foo成员函数的定义,而不是标题中的定义. (90认同)
  • 我不认为这解释了这个问题,显然,关键的问题显然与编辑单元有关,本文中未提及 (29认同)
  • "编译器需要访问方法的实现,使用模板参数实例化它们(在本例中为int).如果这些实现不在标题中,则它们将无法访问"但为什么要实现编译器无法访问.cpp文件?编译器还可以访问.cpp信息,如何将它们转换为.obj文件?编辑:回答这个问题是在这个答案中提供的链接... (11认同)
  • @Gabson:结构和类是等价的,但类的默认访问修饰符是"私有",而结构是公共的.您可以通过查看[此问题](http://stackoverflow.com/q/92859/20984)了解其他一些细微差别. (5认同)
  • 我在这个答案的最开头添加了一个句子,以澄清这个问题是基于一个错误的前提.如果有人问"为什么X是真的?" 当事实上X不是真的时候,我们应该很快拒绝这个假设. (3认同)

MaH*_*uJa 239

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

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

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

template class vector<int>;
Run Code Online (Sandbox Code Playgroud)

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

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

  • 啊.答案很好,但没有真正干净的解决方案.列出模板的所有可能类型似乎不符合模板应该是什么. (46认同)
  • 在许多情况下这可能很好,但通常会破坏模板的目的,这意味着允许您使用任何`类型`的类而无需手动列出它们. (6认同)
  • `vector`不是一个很好的例子,因为容器固有地以"所有"类型为目标.但是,您经常会创建仅适用于特定类型集的模板,例如数字类型:int8_t,int16_t,int32_t,uint8_t,uint16_t等.在这种情况下,使用模板仍然有意义,但是在整个类型集中显式实例化它们也是可能的,在我看来,建议. (6认同)

Ben*_*Ben 232

这是因为需要单独编译,因为模板是实例化式多态.

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

  • foo.h中
    • 声明接口 class MyClass<T>
  • Foo.cpp中
    • 定义了实现 class MyClass<T>
  • bar.cpp
    • 使用 MyClass<int>

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

当我编译foo.cpp时,bar.cpp甚至不需要存在,但是我仍然可以链接foo.o我已经和bar一起了.我刚刚制作了,而不需要重新编译foo .cpp.foo.cpp甚至可以被编译成一个动态库,在没有foo.cpp的情况下分布在其他地方,并且在我编写foo.cpp之后的几年内与它们编写的代码相关联.

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

因此,当编译foo.cpp时,编译器无法看到bar.cpp知道MyClass<int>需要它.它可以看到模板MyClass<T>,但它不能发出代码(它是模板,而不是类).当编译bar.cpp时,编译器可以看到它需要创建一个MyClass<int>,但它看不到模板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>,以便编译器可以MyClass<BazPrivate>在编译baz.cpp期间生成.

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

  • 强调引用**模板实际上是一个模板; 类模板不是类,它是为我们遇到的每个T创建一个新类的方法** (42认同)
  • @ajeh这不是夸夸其谈.问题是"为什么你必须在标题中实现模板?",所以我解释了C++语言导致这一要求的技术选择.在我写答案之前,其他人已经提供了不是完整解决方案的解决方法,因为*不能*是一个完整的解决方案.我觉得这些答案可以通过更充分地讨论问题的"为什么"角度来补充. (10认同)
  • @VoB是的,从这个意义上说,“.tpp”文件只是一种头文件的命名约定。“头文件”并不是 C++ 编译器特有的东西,它只是我们所说的文件,我们打算使用“#include”将其包含到其他编译单元中。如果它可以帮助您处理代码,将模板实现放在与描述 .cpp 文件接口的文件不同的文件中,并为这些模板实现文件提供特定的扩展名(如“.tpp”),那么就去做吧!编译器不知道也不关心其中的差异,但它可以帮助人类。 (3认同)

Dav*_*nak 75

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

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

  • export不会消除源代码泄露的需要,也不会减少编译依赖性,而需要编译器构建器的大量工作.因此Herb Sutter本人要求编译器构建者"忘记"出口.因为所需的时间投资会更好地花在其他地方...... (5认同)
  • 如果您对此感兴趣,那么这篇论文被称为"为什么我们买不起出口",它在他的博客(http://www.gotw.ca/publications)上列出,但没有pdf(快速谷歌应该把它翻过来) ) (3认同)
  • 所以我认为出口尚未实施.在其他人看到它花了多长时间之后,它可能永远不会被EDG以外的任何人完成,并且获得了多少 (2认同)

Dev*_*lar 58

实际上,之前C++ 11标准中定义的export关键字使其能够在头文件中声明的模板和其他地区实施.

没有一个流行的编译器实现了这个关键字.我所知道的唯一一个是Edison Design Group编写的前端,它由Comeau C++编译器使用.所有其他人都要求您在头文件中编写模板,因为编译器需要模板定义才能进行正确的实例化(正如其他人已经指出的那样).

因此,ISO C++标准委员会决定export使用C++ 11 删除模板的功能.

  • ......几年之后,我终于明白了`出口'实际上会给我们什么,给我们什么,什么不是......现在我全心全意地同意EDG人:[它不会有给我们带来了大多数人(我自己在'11包括在内)*认为*它会,如果没有它,C++标准会更好.](http://www.open-std.org/jtc1/sc22/wg21/docs/论文/ 2003/n1426.pdf) (5认同)
  • @DevSolar:这篇论文是政治性的,重复性的并且写得不好。那不是通常的标准水平散文。毫不费力地冗长而无聊,在数十页上说相同的事情基本上是三倍。但是我现在被告知出口不是出口。那是一个很好的英特尔! (3认同)

Ant*_*lev 35

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

有一个导出关键字可以缓解这个问题,但它远不是可移植的.

  • 这几乎是_accurate_答案,除了"这意味着那些编译器不允许在非头文件中定义它们,例如.cpp文件"显然是错误的. (8认同)
  • 你可以,而且你甚至不必把"内联".但是你可以在cpp文件中使用它们而不是其他地方. (2认同)

Ger*_*ago 28

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

export关键字有一个功能,用于单独编译.该export功能已弃用C++11,AFAIK中只有一个编译器实现了该功能.你不应该使用export.单独的编译是不可能的C++或者C++11但也许C++17,如果概念使其在,我们可以有单独的编译的一些方法.

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

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


Ben*_*oît 15

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

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
Run Code Online (Sandbox Code Playgroud)


laf*_*blu 13

即使上面有很多好的解释,我也错过了将模板分成标题和正文的实用方法.
我主要担心的是当我更改其定义时,避免重新编译所有模板用户.
模板体中的所有模板实例化对我来说都不是一个可行的解决方案,因为模板作者可能不知道它的用法和模板用户是否有权修改它.
我采用了以下方法,该方法也适用于较旧的编译器(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
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

MyInstantiatedTemplate.h:

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

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif
Run Code Online (Sandbox Code Playgroud)

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;
Run Code Online (Sandbox Code Playgroud)

main.cpp中:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我喜欢这种方法,但“MyInstantiatedTemplate.h”文件除外并添加了“MyInstantiatedTemplate”类型。如果你不使用它,它会更干净一点,恕我直言。查看我对另一个问题的回答,显示这一点:/sf/answers/2890492601/ (2认同)

Mos*_*aev 9

当您在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。在编译和链接过程中,.cpp 文件被转换为纯对象或机器代码,其中包含引用或未定义的符号,因为包含在 main.cpp 中的 .h 文件还没有实现。这些已准备好与另一个定义模板实现的目标文件链接,因此您有一个完整的 a.out 可执行文件。

但是,由于模板需要在编译步骤中进行处理,以便为您定义的每个模板实例生成代码,因此简单地编译与其头文件分开的模板是行不通的,因为它们总是齐头并进,这是出于这个原因每个模板实例化都是一个全新的类。在常规类中,您可以将 .h 和 .cpp 分开,因为 .h 是该类的蓝图,而 .cpp 是原始实现,因此可以定期编译和链接任何实现文件,但是使用模板 .h 是如何使用的蓝图类应该看起来不是对象应该是什么样子,这意味着模板 .cpp 文件不是类的原始常规实现,它只是类的蓝图,因此 .h 模板文件的任何实现都可以'

因此模板永远不会单独编译,并且只会在您在其他源文件中有具体实例的地方编译。但是具体的实例化需要知道模板文件的实现,因为简单的修改typename T在 .h 文件中使用具体类型不会完成这项工作,因为 .cpp 是要链接的,稍后我找不到它,因为记住模板是抽象的并且无法编译,所以我被迫现在就给出实现,所以我知道要编译和链接什么,现在我有了实现,它被链接到封闭的源文件中。基本上,当我实例化一个模板时,我需要创建一个全新的类,如果我不知道该类在使用我提供的类型时应该是什么样子,我就不能这样做,除非我通知编译器模板实现,所以现在编译器可以T用我的类型替换并创建一个可以编译和链接的具体类。

总而言之,模板是类的外观蓝图,类是对象外观的蓝图。我无法将模板与其具体实例分开编译,因为编译器只编译具体类型,换句话说,模板至少在 C++ 中是纯语言抽象。可以这么说,我们必须去抽象模板,我们通过给它们一个具体的类型来处理,这样我们的模板抽象就可以转换成一个常规的类文件,反过来,它可以正常编译。将模板 .h 文件和模板 .cpp 文件分开是没有意义的。这是荒谬的,因为 .cpp 和 .h 的分离只是 .cpp 可以单独编译和单独链接的地方,使用模板,因为我们无法单独编译它们,因为模板是一种抽象,

意思typename T是在编译步骤而不是链接步骤中T被替换,所以如果我尝试编译模板而不被替换为对编译器完全没有意义的具体值类型,因此无法创建对象代码,因为它没有知道是什么T

在技​​术上可以创建某种功能来保存 template.cpp 文件并在它在其他来源中找到它们时切换类型,我认为该标准确实有一个关键字export可以让您将模板放在单独的cpp 文件,但实际上没有多少编译器实现了这一点。

顺便提一下,在对模板类进行特化时,您可以将标题与实现分开,因为定义特化意味着我专门针对可以单独编译和链接的具体类型。


Nik*_*kos 9

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


我的队列.hpp:

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

    bool isEmpty();

    ...
}    
Run Code Online (Sandbox Code Playgroud)

我的队列.cpp:

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


main()
{
    QueueA<char> Q;

    ...
}
Run Code Online (Sandbox Code Playgroud)

  • 对于真正的男人???如果这是真的,那么您的答案应该被检查为正确的。如果您可以在 .cpp 中定义非模板成员方法,为什么有人需要所有这些 hacky voodo 的东西? (3认同)
  • 这个确切的示例有效,但是除了“myQueue.cpp”之外,您无法从任何其他翻译单元调用“isEmpty”... (2认同)

小智 7

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

  • 这个回应应该更多地修改。我“ _independently_”发现了您的相同方法,并正在特别寻找其他人已经使用它,因为我很好奇它是否是“官方模式”以及是否有名称。我的方法是在需要实现“模板类X”的任何地方实现“类XBase”,将类型相关的部分放在“ X”中,所有其余部分放在“ XBase”中。 (2认同)

小智 6

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

  • "头文件未编译" - 这是一种非常奇怪的描述方式.头文件可以是翻译单元的一部分,就像"c/cpp"文件一样. (5认同)
  • 事实上,这几乎与事实相反,即头文件非常频繁地被编译多次,而源文件通常被编译一次。 (3认同)

Pra*_*nay 6

一种单独实现的方法如下。

//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>
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • s/inner_foo/foo/g 并在 foo.h 的末尾包含 foo.tpp。少一份文件。 (4认同)