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

2256

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

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

为什么是这样呢?

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


29
将所有模板函数定义放在头文件中确实是使用它们最方便的方式,但是引用中的“inline”到底是什么意思还不清楚。没有必要为此使用内联函数,“inline”与此毫无关系。 - AnT stands with Russia
17
书已经过时。 - gerardw
17
一个模板不同于一个可以编译成字节码的函数,它只是一种生成这种函数的模式。如果你把一个模板单独放在一个 *.cpp 文件中,就没有什么东西可以编译。此外,显式实例化实际上并不是一个模板,而是将模板转化为函数的起点,最终生成一个 *.obj 文件。 - dgrat
44
因为这个原因,我是否是唯一一个感觉C++中的模板概念受到了损害的人? - DragonGamer
4
@AnT 或许他们所说的“inline”并不是关键字,而是指“在类内部声明时实现的方法”。 - Vorac
显示剩余5条评论
19个回答

1974
注意:将实现放在头文件中并非必需,可以参考本答案末尾的替代方案。
无论如何,你的代码失败的原因是,在实例化模板时,编译器会根据给定的模板参数创建一个新的类。例如:
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++超级常见问题解答

122
实际上,显式实例化需要在一个可以访问到所有Foo成员函数定义的.cpp文件中进行,而不是在头文件中进行。 - Mankarse
19
编译器需要访问方法的实现,以便使用模板参数(在这种情况下为int)进行实例化。如果这些实现不在头文件中,它们将无法被访问。为什么.cpp文件中的实现对编译器不可见呢?编译器也可以访问.cpp信息,否则它怎么能将它们转换成.obj文件呢? - xcrypt
46
我认为这篇文章没有清楚地解释问题,关键的事情显然与编译单元有关,但这篇文章没有提到。 - zinking
7
@Gabson:结构体和类是等价的,除了默认的访问修饰符不同。类的默认访问修饰符是“private”,而结构体是“public”。还有一些微小的差异,您可以通过查看此问题来了解更多细节(https://dev59.com/pnVD5IYBdhLWcg3wGHeu)。 - Luc Touraille
5
我已在答案开头添加了一句话,以澄清问题基于错误前提。如果有人问“为什么X是真的?”而实际上X不是真的,我们应该迅速拒绝这种假设。 - Aaron McDaid
显示剩余39条评论

348
这是因为需要分离编译,而模板是实例化多态。让我们更加具体地解释一下。假设我有以下文件:
- foo.h:声明了 `class MyClass` 的接口。 - foo.cpp:定义了 `class MyClass` 的实现。 - bar.cpp:使用了 `MyClass`。
分离编译意味着我应该可以独立编译 `foo.cpp` 和 `bar.cpp`。编译器在每个编译单元上进行分析、优化和代码生成,完全独立地完成所有耗费工作;我们不需要进行整个程序的分析。只有连接器需要一次处理整个程序,而且连接器的工作要简单得多。
当我编译 `foo.cpp` 时,`bar.cpp` 甚至不需要存在,但我仍然可以将之前已经拥有的 `foo.o` 与刚刚生成的 `bar.o` 进行链接,而无需重新编译 `foo.cpp`。`foo.cpp` 甚至可以编译成动态库,在没有 `foo.cpp` 的情况下在其他地方分发,并且可以在我编写 `foo.cpp` 几年后链接到他们编写的代码中。
“实例化多态”意味着模板 `MyClass` 实际上并不是一个可以为任何值的 `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 时,它们就可以连接并正常工作。我们可以利用这个事实,通过编写单个模板来实现有限集合的模板实例化。但是,bar.cpp 没有办法使用模板作为模板,并在任何类型上进行实例化;它只能使用foo.cpp 的作者提供的预先存在的模板类的版本。
你可能认为,在编译模板时,编译器应该“生成所有版本”,并在链接期间过滤掉从未使用的版本。除了巨大的开销和这种方法面临的极端困难,因为“类型修饰符”功能(如指针和数组)甚至允许内置类型产生无限数量的类型,还会发生什么情况,当我通过添加以下内容来扩展我的程序时:
  • baz.cpp
    • 声明和实现 class BazPrivate,并使用 MyClass<BazPrivate>
除非我们:
  1. 每次更改程序中的任何其他文件时都必须重新编译 foo.cpp,以防它添加了新的 MyClass<T> 的新实例化。
  2. 要求 baz.cpp 包含(可能通过头文件包含)MyClass<T>的完整模板,以便编译器可以在编译 baz.cpp 时生成 MyClass<BazPrivate>
没有人喜欢(1),因为整个程序分析编译系统需要很长时间进行编译,并且因为它使得无法在没有源代码的情况下分发已编译的库。因此,我们采用(2)。

83
“模板”就是一个模板;“类模板”不是一个类,而是为每个我们遇到的T创建新类的配方。” - v.oddou
21
@ajeh 这并不是花言巧语。问题是“为什么必须在头文件中实现模板?”,所以我解释了C++语言使这种要求成为必需的技术选择。在我回答之前,其他人已经提供了不完整的解决方法,因为 没有 完整的解决方案。我认为这些答案会通过更全面地讨论问题的“原因”角度来补充完善。 - Ben
1
想象一下,如果你不使用模板(来高效地编写所需的代码),那么你只能提供该类的几个版本。因此,你有三个选择。1)不使用模板。(像所有其他类/函数一样,没人在意别人无法更改类型)2)使用模板,并记录可以使用哪些类型。3)给他们整个实现(源代码)奖励4)给他们整个源代码,以防他们想从你的另一个类中制作模板 ;) - Puddle
1
喜欢这个答案,因为它清楚地解释了“为什么?”。 - ani627
3
是的,从那个意义上来说,.tpp 文件只是一种头文件的命名规范。"头文件" 并不是特定于 C++ 编译器的东西,它只是我们称呼打算通过使用 #include 包含到其他编译单元的文件。如果将模板实现放在一个与 .cpp 文件描述接口不同的文件中,并给这些模板实现文件一个特定的扩展名(如 .tpp)可以帮助你处理代码,那就这样做吧!编译器不知道或关心这种区别,但它对人类有所帮助。 - Ben
显示剩余7条评论

276

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

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

编辑:添加显式模板实例化示例。在定义模板和所有成员函数之后使用。

template class vector<int>;

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

上述示例相当无用,因为向量在头文件中完全定义,除非一个常见的包含文件(预编译头?)使用extern template class vector<int>以避免在使用向量的所有其他(1000?)文件中实例化它。


80
呃,回答不错,但没有真正的干净解决方案。列出模板的所有可能类型似乎与模板应该具有的特性不符。 - Jiminion
10
在许多情况下,这可能是有好处的,但通常会破坏模板的目的,该模板旨在允许您使用与任何“类型”匹配的类,而无需手动列出它们。 - Tomáš Zato
12
“vector”并不是一个好的例子,因为容器本质上是针对“所有”类型而设计的。但是,很常见的情况是创建只针对特定一组类型(比如数字类型:int8_t、int16_t、int32_t、uint8_t、uint16_t等)的模板。在这种情况下,仍然有意义使用模板,但明确地为整个类型集实例化它们也是可能的,并且我认为这是推荐的做法。 - UncleZeiv
显式实例化必须在模板方法的实现可见的地方。如果你的实现在cpp文件中,那么(不要傻乎乎的)你只能在同一个cpp文件中显式实例化,而不能在任何其他cpp文件中这样做。 - Miral
1
我感觉好像漏了什么... 我把两种类型的显式实例化放到类的 .cpp 文件中,这两个实例化被其他 .cpp 文件引用,但我仍然得到链接错误,说找不到成员。 - oarfish
显示剩余3条评论

95

在将模板编译为目标代码之前,编译器需要先实例化模板。只有当模板参数已知时才能进行实例化。假设有这样一种情况:在a.h中声明了一个模板函数,在a.cpp中定义该函数并在b.cpp中使用它。当编译a.cpp时,并不确定是否需要实例化模板,更不知道具体的实例是哪个。对于更多的头文件和源文件,情况会变得更加复杂。

有人可以提出,编译器可以变得更智能,预见到所有使用该模板的情况。但是我相信很容易创建递归或其他复杂的场景,让编译器难以预测。据我的了解,编译器并不会进行这种先见性的查找。正如Anton所指出的,一些编译器支持显式导出模板实例的声明,但并非所有编译器都支持(尚未?)。


1
"export"是标准用语,但实际上它很难实现,所以大多数编译器团队都还没有完成。 - vava
6
导出并不能消除源码披露的需求,也不能减少编译依赖,同时还需要编译器构建者付出大量的努力。所以Herb Sutter本人要求编译器构建者“忘记”导出。因为在其他地方投资所需的时间会更好。 - Pieter
2
所以我认为导出功能还没有被实现。其他人看到花费的时间和收益很少后,可能永远不会完成,除非EDG自己完成。 - Pieter
3
如果您感兴趣的话,这篇论文叫做《为什么我们负担不起出口》,可以在他的博客(http://www.gotw.ca/publications)上找到,但那里没有PDF版本(不过快速谷歌搜索应该能找到)。 - Pieter
1
好的,感谢您提供的良好示例和解释。但是我有一个问题:为什么编译器无法确定模板被调用的位置,并在编译定义文件之前先编译这些文件呢?我可以想象在简单情况下可以这样做... 那么答案是因为相互依赖会很快混乱吗? - Vlad
显示剩余2条评论

76
事实上,在C++11之前,标准定义了export关键字,可以在头文件中声明模板并在其他地方实现它们。某种程度上是这样。但实际上不是这样的,因为唯一一个指出该特性的人是:

幻影优势#1:隐藏源代码。许多用户表示,他们希望使用导出功能后,将不再需要为成员/非成员函数模板和类模板的成员函数提供定义。但这是不正确的。使用导出功能,库编写者仍然必须提供完整的模板源代码或其直接等效物(例如,系统特定的解析树),因为完整信息是实例化所必需的。[...]

幻影优势#2:快速构建、减少依赖。许多用户期望导出功能将允许真正将模板分别编译为目标代码,从而实现更快的构建。但实际上并不是这样的,因为导出模板的编译确实是分开的,但不是编译为目标代码。相反,导出几乎总是使构建变慢,因为至少仍需要在预链接时间完成同样数量的编译工作。导出甚至不能减少模板定义之间的依赖性,因为这些依赖性是内在的,独立于文件组织。

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

因此,ISO C++标准委员会决定在C++11中删除模板的export特性。

9
几年后,我终于明白了export实际上给了我们什么,以及没有给我们什么...现在,我从心底赞同EDG的人们:它不会带给我们大多数人(包括我在'11年时)认为的东西,C++标准没有它更好。 - DevSolar
6
@DevSolar :这篇文章具有政治性,重复且写得很糟糕。那不是通常标准的优秀散文水平。文章过长并且令人厌倦,在数十页中基本上重复了三次相同的内容。但现在我被告知出口并非真正的出口。这是一个好情报! - v.oddou
1
@v.oddou:优秀的开发人员和优秀的技术作家是两种不同的技能。有些人两者都能做到,但很多人不能。;-) - DevSolar
1
@v.oddou 这篇论文不仅写得很糟糕,而且是一种误导。此外,它对现实进行了扭曲:实际上支持出口的极其强有力的论点被混杂在一起,使其听起来像是反对出口:“在出口存在的情况下,在标准中发现了许多与ODR相关的漏洞。在出口之前,编译器不必诊断ODR违规行为。现在必须这样做,因为您需要将来自不同翻译单元的内部数据结构组合在一起,如果它们实际上代表不同的事物,则无法将它们组合在一起,因此需要进行检查。” - curiousguy
1
@DevSolar 我仍然没有在论文中看到反对导出的论据。(我看到了支持导出的论据。) - curiousguy
显示剩余4条评论

42

尽管标准C++没有这样的要求,但一些编译器需要在每个使用到函数和类模板的翻译单元中都提供它们。因此,在这些编译器中,模板函数的实现必须在头文件中提供。需要强调的是,这意味着这些编译器不允许将其定义在非头文件(例如.cpp文件)中。

有一个“export”关键字可以缓解这个问题,但它远远不能做到可移植。


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

37

模板经常用在头文件中,因为编译器需要根据传递/推断的模板参数实例化不同版本的代码,并且让编译器多次重新编译相同的代码并稍后去重更容易(作为程序员)。

请记住,模板不直接代表代码,而是代表该代码的几个版本的模板。

.cpp 文件中编译非模板函数时,您正在编译一个具体的函数/类。但对于模板来说情况并非如此,因为它可以使用不同类型进行实例化,即替换模板参数与具体类型时必须发射具体代码。

export 关键字有一个功能,用于分离编译。 export 功能在 C++11 中已被弃用,并且仅有一种编译器实现了它。不应使用 export。在 C++C++11 中无法实现单独的编译,但在 C++17 中,如果概念得以实现,我们可能会有一些方法实现单独的编译。

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

我猜分离编译问题也是随着迁移到模块而出现的问题,目前正在努力解决它。

编辑:截至 2020 年 8 月,模块已成为 C++ 的现实:https://en.cppreference.com/w/cpp/language/modules


目前我知道常见编译器中没有模块的实现。 - chadmc

27
尽管上面有很多好的解释,但我还是缺少一种将模板分离为头部和主体的实用方法。
我的主要问题是当我更改模板定义时,避免重新编译所有模板用户。
在模板主体中具有所有模板实例化不是一个可行的解决方案,因为模板作者可能不知道它的所有用法,而模板用户可能没有修改它的权限。
我采取了以下方法,它也适用于旧的编译器(例如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;
}

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


2
我喜欢这种方法,除了MyInstantiatedTemplate.h文件和添加的MyInstantiatedTemplate类型之外。如果不使用它,会更加简洁。请查看我在另一个问题上的答案,展示了这一点:https://dev59.com/bHVD5IYBdhLWcg3wBWzd#41292751 - Cameron Tacklind
这将结合两个最好的方面。我希望这个答案能够得到更高的评价!同时请参考上面的链接,以获取同样想法的稍微更清晰的实现。 - Wormer

18

这意味着在模板类定义内部定义方法实现是最具可移植性的方式。

template < typename ... >
class MyClass
{

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

12
当您在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。在编译和链接过程中,.cpp文件会被转换为纯粹的目标或机器代码,其中包含引用或未定义的符号,因为在main.cpp中包含的.h文件尚未实现。它们准备与定义模板实现的另一个对象文件连接起来,因此您可以获得完整的a.out可执行文件。
然而,由于需要在编译步骤中处理模板以生成您定义的每个模板实例化的代码,因此仅编译模板而不与其头文件一起使用是行不通的,因为它们始终是一对,因为每个模板实例化实际上是一个全新的类。在常规类中,您可以分离.h和.cpp,因为.h是该类的蓝图,而.cpp是原始实现,因此任何实现文件都可以正常编译和链接。但是,在使用模板时,.h是该类应该如何看起来的蓝图,而不是对象应该如何看起来的蓝图,这意味着模板.cpp文件不是类的普通生实现,而只是类的蓝图,因此任何.h模板文件的实现都无法编译,因为您需要具体内容才能编译,模板在这个意义上是抽象的。
因此,模板永远不会被单独编译,只有在其他源文件中具有具体实例化时才会被编译。但是,具体实例化需要知道模板文件的实现,因为简单地修改typename T在.h文件中使用具体类型并不能完成工作,因为.cpp在哪里进行链接,我无法在以后找到它,因为请记住,模板是抽象的,无法编译,因此我现在被迫提供实现,因此我知道要编译和链接什么,现在,我有了实现,它被链接到封闭源文件中。基本上,当我实例化模板时,我需要创建一个全新的类,如果我不知道在使用我提供的类型时该类应该看起来像什么,那么我就不能这样做,除非我让编译器注意模板实现,因此现在编译器可以使用我的类型替换 T 并创建一个准备好编译和链接的具体类。
总之,模板是类应该如何看起来的蓝图,类是对象应该如何看起来的蓝图。我不能将模板与其具体实例分开编译,因为编译器只编译具体类型,换句话说,在C++中,模板至少是纯语言抽象。我们必须解除模板的抽象,通过给它们一个具体的类型来解决问题,以便我们的模板抽象可以转化为常规类文件,从而可以正常编译。分离模板.h文件和模板.cpp文件没有意义,这是无意义的,因为.h和.cpp的分离只能单独编译和链接.cpp,但由于我们无法单独编译模板,因为模板是抽象的,因此我们总是被迫将抽象部分始终与具体实例一起使用,其中具体实例始终必须了解正在使用的类型。 < p >意义在于编译过程中替换typename T,而不是在链接过程中。因此,如果我尝试编译一个模板,而T没有被替换为一个具体的值类型,那对于编译器来说就是毫无意义的,结果就是无法创建对象代码,因为它不知道T是什么。

从技术上讲,可以创建某种功能,当发现其他源中的类型时,会保存template.cpp文件并切换类型,我认为标准确实有一个关键字export,它将允许您将模板放在单独的cpp文件中,但实际上并没有太多编译器实现这一点。

顺便提一下,当为模板类制作专门化时,可以将头文件与实现分开,因为根据定义,专门化意味着我正在为可以单独编译和链接的具体类型进行专门化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接