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


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

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

为什么是这样?

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


1386
2018-01-30 10:06


起源


问题不正确。还有另一种便携方式。模板类可以显式实例化 - 正如其他答案所指出的那样。 - Aaron McDaid
虽然将所有模板函数定义放入头文件确实是使用它们最方便的方法,但仍然不清楚该内容中的“内联”是什么。没有必要使用内联函数。 “内联”与此完全无关。 - AnT
书已过期。 - gerardw


答案:


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

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

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 ++超级常见问题解答


1208
2018-01-30 10:26



实际上,显式实例化需要在.cpp文件中,该文件可以访问所有Foo成员函数的定义,而不是标题中的定义。 - Mankarse
“编译器需要访问方法的实现,使用模板参数实例化它们(在本例中为int)。如果这些实现不在标题中,则它们将无法访问”但为什么要实现编译器无法访问.cpp文件?编译器还可以访问.cpp信息,如何将它们转换为.obj文件?编辑:回答这个问题是在这个答案中提供的链接... - xcrypt
我不认为这解释了这个问题,显然,关键的问题显然与编辑单元有关,本文中未提及 - zinking
@Gabson:结构和类是等价的,但类的默认访问修饰符是“私有”,而结构是公共的。您还可以通过查看来了解其他一些微小差异 这个问题。 - Luc Touraille
使用显式实例化,否则很快就会得到非常慢的构建。 - Nils


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

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

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

template class vector<int>;

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

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


200
2017-08-13 13:49



啊。答案很好,但没有真正干净的解决方案。列出模板的所有可能类型似乎不符合模板应该是什么。 - Jiminion
在许多情况下这可能是好的,但通常会破坏模板的目的,这意味着允许您使用任何类 type 没有手动列出它们。 - Tomáš Zato
vector 不是一个很好的例子,因为容器固有地以“所有”类型为目标。但是,您经常会创建仅适用于特定类型集的模板,例如数字类型:int8_t,int16_t,int32_t,uint8_t,uint16_t等。在这种情况下,使用模板仍然有意义,但是在整个类型集中明确地实例化它们也是可能的,在我看来,推荐。 - UncleZeiv
在定义模板后使用,“并且已定义所有成员函数”。谢谢 ! - Vitt Volt
我想知道,是否可以从类的头文件或源文件以外的地方进行显式实例化?例如,在main.cpp中执行它们吗? - gromit190


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

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

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

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

bar.cpp 在编译时甚至不需要存在 Foo.cpp中,但我仍然可以链接 foo.o的 我已经在一起了 文件bar.o 我刚刚制作,无需重新编译 Foo.cpp中Foo.cpp中 甚至可以编译成动态库,在其他地方分发 Foo.cpp中,并与我写的几年后编写的代码相关联 Foo.cpp中

“Instantiation-style polymorphism”表示模板 MyClass<T> 实际上并不是一个可以编译为可以适用于任何值的代码的泛型类 T。这会增加开销,例如装箱,需要将函数指针传递给分配器和构造器等.C ++模板的目的是避免编写几乎相同的 class MyClass_intclass 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)。


173
2018-05-11 03:54



强调报价 模板实际上是一个模板;类模板不是类,它是为我们遇到的每个T创建一个新类的配方 - v.oddou
我想知道,是否可以从类的头文件或源文件以外的地方进行显式实例化?例如,在main.cpp中执行它们吗? - gromit190
@Birger您应该可以从任何有权访问完整模板实现的文件中执行此操作(因为它位于同一文件中或通过包含头文件)。 - Ben
这是一个怎样的答案?它没有提供任何解决方案,只提供修辞。 - ajeh
@ajeh这不是夸夸其谈。问题是“为什么你必须在标题中实现模板?”,所以我解释了C ++语言导致这一要求的技术选择。在我写回答之前,其他人已经提供了不完全解决方案的解决方法,因为那里 不能 是一个完整的解决方案我觉得这些答案可以通过更全面地讨论问题的“为什么”角度来补充。 - Ben


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

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


64
2018-01-30 10:23



“export”是标准的,但它很难实现,所以大多数编译器团队还没有完成。 - vava
export不会消除源代码泄露的需要,也不会减少编译依赖性,而需要编译器构建器的大量工作。因此Herb Sutter本人要求编译器构建者“忘记”出口。因为所需的时间投资会更好地花在其他地方...... - Pieter
所以我认为出口尚未实施。在其他人看到它花了多长时间之后,它可能永远不会被EDG以外的任何人完成,并且获得了多少 - Pieter
如果你对此感兴趣,那么这篇论文被称为“为什么我们买不起出口”,它在他的博客上列出(gotw.ca/publications)但没有pdf(快速谷歌应该把它打开) - Pieter
好的,谢谢你的好例子和解释。这是我的问题:为什么编译器无法确定调用模板的位置,并在编译定义文件之前先编译这些文件?我可以想象它可以在一个简单的案例中完成......相互依赖性的答案是否会让订单变得非常快? - Vlad


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

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

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


47
2018-01-30 13:38



结果导出模板已从C ++ 11中删除。 - johannes
@johannes:我没听清楚,谢谢。实际上,你无法使用 export 无论如何,因为这会使你与Comeau编译器联系在一起。这是一个“死亡的特征”;只是一个我会喜欢看到无处不在实施的。 - DevSolar
......几年后,我 最后 明白了什么 export 实际上会有 特定 我们,什么不是......现在我全心全意地同意EDG人员: 它不会给我们带来什么大多数人(我自己在'11包括在内) 认为 如果没有它,C ++标准会更好。 - DevSolar
@DevSolar:这篇论文是政治的,重复的,写得不好。这不是通常的标准级散文。无聊长而无聊,基本上说了几十页同样的事情。但我现在知道出口不是出口。这是一个很好的英特尔! - v.oddou
@ v.oddou:优秀的开发人员和优秀的技术作家是两个独立的技能组合。有些人可以做到,两者都做不到。 ;-) - DevSolar


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

有一个 出口 应该用来缓解这个问题的关键字,但它无法轻松移植。


31
2018-01-30 10:15



为什么我不能在.cpp文件中使用关键字“inline”实现它们? - MainID
你可以,而且你甚至不必把“内联”。但是你可以在cpp文件中使用它们而不是其他地方。 - vava
这几乎是最多的 准确 回答,除了“这意味着那些编译器不允许在非头文件中定义它们,例如.cpp文件”显然是错误的。 - Lightness Races in Orbit


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

有一个功能 export 用于单独编译的关键字。 该 export 功能已弃用 C++11 而且,AFAIK,只有一个编译器实现了它。你不应该使用 export。无法进行单独编译 C++ 要么 C++11 但也许在 C++17如果概念出现,我们可以采用某种方式进行单独的编译。

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

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


26
2018-05-12 16:42





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

template < typename ... >
class MyClass
{

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

13
2018-01-30 10:53





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





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


6
2017-09-17 03:40



“头文件未编译” - 这是一种非常奇怪的描述方式。头文件可以是翻译单元的一部分,就像“c / cpp”文件一样。 - Flexo♦
实际上,它几乎与事实相反,即头文件经常编译很多次,而源文件通常编译一次。 - xaxxon