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


1775

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

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

为什么是这样?

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


13
虽然将所有模板函数定义放入头文件中确实是使用它们的最方便的方法,但仍不清楚该引用中的“内联”功能是什么。无需为此使用内联函数。“内联”绝对与此无关。
2014年

7
书已过期。
gerardw

1
模板不同于可以编译为字节码的函数。这只是产生这种功能的一种模式。如果将模板单独放入* .cpp文件,则没有任何可编译的内容。而且,显式实例化实际上不是模板,而是从模板制作功能的起点,该功能最终在* .obj文件中。
dgrat

5
我是唯一一个因此感到模板概念在C ++中瘫痪的人吗?
DragonGamer

Answers:


1554

警告:这是没有必要把落实在头文件中,看到这个答案的末尾替代解决方案。

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

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 */}
}

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

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

oo

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

#include "Foo.tpp"

Foo.tpp

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

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

替代解决方案

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

oo

// 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


96
实际上,显式实例化必须在.cpp文件中,该文件可以访问Foo的所有成员函数的定义,而不是标头。
Mankarse 2011年

11
“编译器需要访问方法的实现,并使用模板参数(在本例中为int)将其实例化。如果这些实现不在头文件中,则将无法访问它们。”但是为什么在.cpp文件无法由编译器访问?编译器也可以访问.cpp信息,否则它将如何将其转换为.obj文件?编辑:这个问题的答案在这个答案中提供的链接...
xcrypt

31
我不认为这可以清楚地解释这个问题,关键的事情显然与本文未提及的UNIT编译有关
zinking 2012年

6
@Gabson:结构和类是等效的,不同的是,类的默认访问修饰符是“私有”,而结构的公共访问修饰符是公共的。通过查看此问题,您还可以了解其他一些微小差异。
2014年

3
我在此答案的开头添加了一个句子,以澄清该问题是基于错误的前提。如果有人问“为什么X为真?” 当实际上X不正确时,我们应该迅速拒绝该假设。
亚伦·麦克戴德

250

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

如果在实现cpp文件的底部,对模板将使用的所有类型进行显式实例化,则链接程序将能够照常查找它们。

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

template class vector<int>;

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

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


51
啊。好的答案,但没有真正的解决方案。列出模板的所有可能类型似乎与模板应该具有的外观不符。
Jiminion

6
这在许多情况下可能会很好,但通常会破坏模板的目的,该目的是允许您将类与任何类一起使用,type而无需手动列出它们。
托马什Zato -恢复莫妮卡

7
vector这不是一个好例子,因为容器本质上是针对“所有”类型的。但是,创建模板仅用于特定类型集的情况确实经常发生,例如数字类型:int8_t,int16_t,int32_t,uint8_t,uint16_t等。在这种情况下,使用模板仍然有意义,但也可以为整个类型的集合显式实例化它们,我认为建议这样做。
UncleZeiv

在定义模板后使用“已定义所有成员函数”。谢谢 !
Vitt Volt

1
我感觉好像丢失了一些东西……我将两种类型的显式实例化放入类的.cpp文件中,并且这两个实例化是从其他.cpp文件中引用的,但仍然收到未找到成员的链接错误。
oarfish

249

这是因为需要单独编译,并且模板是实例化样式的多态性。

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

  • 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之后数年编写的代码链接。

“实例化样式多态性”表示模板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)。


50
强调引用模板实际上是模板;一个班级模板不是一个班级,而是为我们遇到的每个T创建一个新班级的秘诀
v.oddou 2016年

我想知道,是否可以从类的标头或源文件之外的其他位置进行显式实例化?例如,它们在main.cpp中吗?
gromit190年

1
@Birger您应该能够从有权访问完整模板实现的任何文件中执行此操作(因为它位于同一文件中,或者通过标头包含)。

11
@ajeh这不是夸夸其谈。问题是“为什么必须在标头中实现模板?”,所以我解释了C ++语言为达到此要求而做出的技术选择。在我写答案之前,其他人已经提供了不是完整解决方案的解决方法,因为没有完整的解决方案。我认为这些答案将得到对该问题“为什么”角度的更充分讨论的补充。

1
大家以这种方式想象一下...如果您不使用模板(有效地编码所需的代码),无论如何您只会提供该类的几个版本。所以你有3个选择 1)。不要使用模板。(像所有其他类/函数一样,没有人关心别人不能改变类型)2)。使用模板,并记录其可以使用的类型。3)。给他们整个实施(来源)奖金4)。如果他们想从另一个班级制作模板,请给他们全部资源;)
水坑

81

在实际将模板编译为目标代码之前,需要由编译器实例化模板。仅在知道模板参数的情况下才能实现此实例化。现在,设想一种在中声明a.h,定义a.cpp和使用模板函数的情况b.cpp。当a.cpp被编译,它不一定知道,即将到来的编纂b.cpp将需要模板的实例,更何况哪个特定实例会是这样。对于更多的头文件和源文件,情况可能很快变得更加复杂。

可以争论的是,可以使编译器变得更聪明,以“预见”模板的所有用途,但是我敢肯定,创建递归或其他复杂的场景并不困难。AFAIK,编译器不会提前这样做。正如Anton所指出的,一些编译器支持模板实例的显式导出声明,但并非所有编译器都支持(实例?)。


1
“导出”是标准的,但是很难实现,因此大多数编译器团队尚未完成。
vava

5
导出并不会消除公开源代码的需求,也不会减少编译依赖性,而它需要编译器构建者付出巨大的努力。因此,Herb Sutter自己要求编译器制造商“放弃”导出。由于所需的时间是在其他地方花费更好的时间……
Pieter 2009年

2
因此,我认为出口尚未实现。在他人看到EDG花费了多长时间之后又获得了很少的收益之后,除EDG之外,其他任何人都可能永远无法做到这一点
Pieter 2009年

3
如果您对此感兴趣,该论文称为“为什么我们负担不起出口费用”,该论文列在他的博客中(gotw.ca/publications),但那里没有pdf文件(不过,快速的Google应该把它打开了)
Pieter

1
好的,谢谢您的良好示例和解释。但是,这是我的问题:为什么编译器无法弄清楚模板的调用位置,而是在编译定义文件之前先编译这些文件?我可以想象它可以在一个简单的情况下完成...相互依赖性会很快弄乱顺序的答案吗?
Vlad13年

62

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

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

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


6
...而且几年后,我终于明白了export实际上会给我们带来什么,而没有我们带来什么...现在我全心全意地同意EDG的想法
DevSolar 2015年

4
@DevSolar:这篇论文是政治性的,重复性的并且写得不好。那不是通常的标准水平散文。毫不费力地冗长而无聊,在数十页上说相同的事情基本上是三倍。但是我现在被告知出口不是出口。那是一个很好的英特尔!
v.oddou '16

1
@ v.oddou:好的开发人员和好的技术作家是两个独立的技能组。有些人可以两者兼而有之。;-)
DevSolar 2016年

@ v.oddou这篇论文不仅写得不好,而且是虚假信息。这也是现实的一个转折:实际上,对出口的极强论点混杂在一起,以某种方式听起来似乎与出口相反:“在存在出口的情况下,发现标准中存在许多与ODR相关的漏洞。在导出之前,编译器不必诊断出违反ODR的情况。现在,这是必要的,因为您需要组合来自不同翻译单元的内部数据结构,并且如果它们实际上表示不同的内容,则不能将其组合,因此您需要进行检查。”
curiousguy19年

现在必须补充它是在什么时候发生哪种翻译单位 ”咄。当您被迫使用la脚的参数时,您将没有任何参数。当然,您会在错误中提及文件名,这有什么关系?任何人都不喜欢那个BS真是令人难以置信。“ 甚至像James Kanze这样的专家也很难接受出口真的是这样。 ”什么?!!
curiousguy19年

34

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

有一个export关键字可以缓解此问题,但是它几乎没有可移植性。


为什么不能在.cpp文件中使用关键字“内联”实现它们?
MainID

2
您可以,甚至不必“内联”。但是您将只能在该cpp文件中使用它们,而在其他任何地方都不能使用它们。
vava

10
这几乎是最准确的答案,除了“这意味着那些编译器不允许在.cpp文件之类的非标头文件中定义它们”是错误的。
Lightness Races in Orbit

28

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

export关键字具有一个功能,该功能旨在用于单独的编译。该export功能已在C++11AFAIK中弃用,只有一个编译器实现了该功能。您不应该使用export。在C++或中C++11可能无法进行单独编译C++17,如果有概念可以实现,则我们可以采用某种方式进行单独编译。

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

模板的单独编译问题我想这也是当前正在工作的向模块迁移的问题。


15

这意味着定义模板类的方法实现的最可移植的方法是在模板类定义中定义它们。

template < typename ... >
class MyClass
{

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

15

尽管上面有很多很好的解释,但我仍然缺少将模板分为标题和正文的实用方法。
我主要关心的是在更改模板定义时避免重新编译所有模板用户。
对我而言,在模板主体中包含所有模板实例化不是一个可行的解决方案,因为模板创建者可能不了解其用法,并且模板用户可能无权对其进行修改。
我采用了以下方法,该方法也适用于较早的编译器(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;
}

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


1
除了MyInstantiatedTemplate.h文件和添加的MyInstantiatedTemplate类型外,我喜欢这种方法。恕我直言,如果您不使用它,那会更清洁。在另一个显示此问题的问题上签出我的答案:stackoverflow.com/a/41292751/4612476
Cameron Tacklind '19

这需要两个世界中最好的一个。我希望这个答案的评分更高!另请参见上面的链接,以更简洁地实现相同的想法。
Wormer

8

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


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;

    ...
}

2
对于真正的男人?如果是真的,那么您的答案应该被检查为正确。如果您只可以在.cpp中定义非模板成员方法,那么为什么有人需要所有这些讨厌的伏都教徒?
Michael IV

嗯,这行不通。至少在MSVC 2019上,为模板类的成员函数获取了无法解析的外部符号。
Michael IV

我没有要测试的MSVC 2019。C ++标准允许这样做。现在,MSVC并不总是遵守规则而臭名昭著。如果还没有,请尝试项目设置-> C / C ++->语言->一致性模式->是(允许-)。
Nikos

1
这个确切的示例有效,但是您不能isEmptymyQueue.cpp... 之外的任何其他翻译部门打电话
MM

7

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


2
这个回应应该更多地修改。我“ 独立地 ”发现了您的相同方法,并专门寻找其他人已经使用它,因为我很好奇这是否是一种正式模式以及它是否有名字。我的方法是class XBase在需要实施a的任何地方实施template class X,将类型相关的部分放进去X,其余的放进去XBase
Fabio A.

6

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


5
“头文件未编译”-这是描述它的一种很奇怪的方式。头文件可以是翻译单元的一部分,就像“ c / cpp”文件一样。
柔印

2
实际上,这几乎与事实相反,那就是头文件经常被编译多次,而源文件通常被编译一次。
xaxxon

6

在编译步骤中使用模板时,编译器将为每个模板实例生成代码。在编译和链接过程中,.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是在编译步骤而不是链接步骤中替换了get,因此,如果我尝试编译模板而不将T其替换为对编译器完全没有意义的具体值类型,则由于无法创建目标代码,因此无法创建该模板知道是什么T

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

附带说明一下,在对模板类进行专业化处理时,可以将标头与实现分开,因为按定义进行的专业化处理意味着我专门针对可以单独编译和链接的具体类型。


4

单独实施的方法如下。

//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或其他代码时,这很烦人。


2
s / inner_foo / foo / g,并在foo.h的末尾包含foo.tpp。少一个文件。

1

我建议看一下这个gcc页面,其中讨论了模板实例化的“ cfront”和“ borland”模型之间的取舍。

https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html

“ borland”模型与作者的建议相对应,提供了完整的模板定义,并已多次编译内容。

它包含有关使用手动和自动模板实例化的明确建议。例如,“-repo”选项可用于收集需要实例化的模板。另一个选择是使用“ -fno-implicit-templates”禁用自动模板实例化,以强制手动模板实例化。

以我的经验,我依靠为每个编译单元实例化的C ++标准库和Boost模板(使用模板库)。对于大型模板类,我会针对所需类型进行一次手动模板实例化。

这是我的方法,因为我提供的是工作程序,而不是供其他程序使用的模板库。该书的作者Josuttis在模板库上进行了大量工作。

如果我真的担心速度,我想我会探索使用预编译头 https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

这在许多编译器中都得到了支持。但是,我认为模板头文件很难预编译头。


-2

在头文件中同时写入声明和定义的一个好主意是出于可读性。假设Utility.h中有这样的模板函数:

template <class T>
T min(T const& one, T const& theOther);

在Utility.cpp中:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

这要求此处的每个T类都实现小于运算符(<)。当您比较尚未实现“ <”的两个类实例时,它将引发编译器错误。

因此,如果您将模板的声明和定义分开,您将无法仅读取头文件来查看此模板的来龙去脉,以便在自己的类上使用此API,尽管编译器会在此告诉您。有关需要重写哪个运算符的案例。


-7

您实际上可以在.template文件而不是.cpp文件中定义模板类。无论谁说您只能在头文件中定义它都是错误的。这可以一直追溯到c ++ 98。

不要忘记让编译器将.template文件视为c ++文件,以保持智能。

这是动态数组类的一个示例。

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

现在,在.template文件中,您可以按通常的方式定义函数。

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }

2
大多数人会将头文件定义为可以将定义传播到源文件的任何文件。因此,您可能已决定使用文件扩展名“ .template”,但已编写了头文件。
汤米
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.