ChatGPT解决这个技术问题 Extra ChatGPT

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

引自 The C++ standard library: a tutorial and handbook

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

为什么是这样?

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

虽然确实将所有模板函数定义放入头文件可能是使用它们的最方便的方法,但仍然不清楚该引用中的“内联”在做什么。没有必要为此使用内联函数。 “内联”与此完全无关。
书已过时。
模板不像可以编译成字节码的函数。生成这样的功能只是一种模式。如果将模板单独放入 *.cpp 文件中,则无需编译。此外,显式实例化实际上不是模板,而是从模板中生成函数的起点,最终在 *.obj 文件中。
我是唯一一个觉得模板概念因此而在 C++ 中被削弱的人吗?...
@AnT 也许他们的意思是“内联”不是关键字,而是“在类内部声明处实现的方法”。

N
NathanOliver

警告:没有必要将实现放在头文件中,请参阅此答案末尾的替代解决方案。

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

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"

文件.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 on this subject


实际上,显式实例化需要在 .cpp 文件中,该文件可以访问 Foo 的所有成员函数的定义,而不是在头文件中。
“编译器需要访问方法的实现,使用模板参数(在本例中为 int)实例化它们。如果这些实现不在标头中,它们将无法访问”但是为什么在编译器无法访问 .cpp 文件?编译器还可以访问 .cpp 信息,否则它将如何将它们转换为 .obj 文件?编辑:这个问题的答案在这个答案中提供的链接中......
我不认为这清楚地解释了这个问题,关键显然与本文未提及的编译 UNIT 有关
@Gabson:结构和类是等效的,除了类的默认访问修饰符是“私有的”,而对于结构来说它是公共的。您可以通过查看 this question 了解其他一些细微差别。
我在这个答案的开头添加了一个句子,以澄清这个问题是基于一个错误的前提。如果有人问“为什么 X 是真的?”当事实上 X 不正确时,我们应该迅速拒绝这个假设。
B
Ben

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

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

foo.h 声明类 MyClass 的接口

声明类 MyClass 的接口

foo.cpp 定义了类 MyClass 的实现

定义类 MyClass 的实现

bar.cpp 使用 MyClass

使用 MyClass

单独编译意味着我应该能够独立于 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编译时,编译器可以看到需要创建一个MyClass<int>,但是看不到模板MyClass<T>(只有它在foo. h) 所以它不能创建它。

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

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

baz.cpp 声明并实现类 BazPrivate,并使用 MyClass

声明并实现类 BazPrivate,并使用 MyClass

除非我们

每次我们更改程序中的任何其他文件时都必须重新编译 foo.cpp,以防它添加了 MyClass 的新实例化 要求 baz.cpp 包含(可能通过标题包含)MyClass 的完整模板, 以便编译器可以在编译 baz.cpp 时生成 MyClass

没有人喜欢(1),因为整个程序分析编译系统需要很长时间才能编译,并且因为它使得在没有源代码的情况下分发编译的库是不可能的。所以我们有(2)。


强调引用一个模板实际上就是一个模板;类模板不是类,它是为我们遇到的每个 T 创建一个新类的秘诀
@Birger您应该能够从可以访问完整模板实现的任何文件中执行此操作(因为它在同一个文件中或通过标题包含)。
@ajeh 这不是修辞。问题是“为什么必须在标头中实现模板?”,所以我解释了 C++ 语言做出的导致此要求的技术选择。在我写答案之前,其他人已经提供了不是完整解决方案的变通办法,因为不可能有完整的解决方案。我觉得这些答案将得到对问题“为什么”角度的更全面讨论的补充。
想象一下,伙计们……如果您不使用模板(以有效地编写所需的代码),那么无论如何您只会提供该类的几个版本。所以你有3个选择。 1)。不要使用模板。 (像所有其他类/函数一样,没有人关心其他人不能改变类型)2)。使用模板,并记录他们可以使用的类型。 3)。给他们整个实施(来源)奖金4)。给他们完整的源代码,以防他们想从你的另一个类中制作模板;)
@VoB 是的,从这个意义上说,.tpp 文件只是一种头文件的命名约定。 “头文件”不是特定于 C++ 编译器的东西,它只是我们所谓的文件,我们打算通过使用 #include 将其包含到其他编译单元中。如果它可以帮助您使用代码将模板实现与描述 .cpp 文件接口的文件放在一个单独的文件中,并为这些模板实现文件提供特定的扩展名,例如 .tpp,那么就去做吧!编译器不知道也不关心差异,但它可以帮助人类。
T
Ted Lyngmo

这里有很多正确的答案,但我想添加这个(为了完整性):

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

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

template class vector<int>;

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

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


啊。很好的答案,但没有真正干净的解决方案。列出模板的所有可能类型似乎与模板应该是什么不符。
这在许多情况下可能很好,但通常会破坏模板的目的,模板的目的是允许您将类与任何 type 一起使用,而无需手动列出它们。
vector 不是一个很好的例子,因为容器本身就以“所有”类型为目标。但是确实经常发生您创建仅适用于特定类型集的模板,例如数字类型:int8_t、int16_t、int32_t、uint8_t、uint16_t 等。在这种情况下,使用模板仍然有意义,但也可以为整个类型集显式实例化它们,并且在我看来,这是推荐的。
在定义模板后使用,“并且所有成员函数都已定义”。谢谢 !
我觉得我错过了一些东西......我将两种类型的显式实例化放入类的 .cpp 文件中,并且这两个实例化是从其他 .cpp 文件中引用的,但我仍然得到成员不是的链接错误成立。
K
K DawG

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

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


“导出”是标准的,但很难实现,所以大多数编译器团队还没有完成。
export 并没有消除对源代码披露的需要,也没有减少编译依赖项,但它需要编译器构建者的巨大努力。因此 Herb Sutter 本人要求编译器构建者“忘记”导出。由于所需的时间投资会更好地花在其他地方......
因此,我认为“尚未”实施出口。在其他人看到它花了多长时间之后,除了 EDG 之外,其他任何人都可能永远不会完成它,并且获得的收益是多么少
如果您对此感兴趣,那篇论文名为“为什么我们负担不起出口”,它列在他的博客 (gotw.ca/publications) 上,但那里没有 pdf(不过,快速谷歌应该会打开它)
好的,谢谢你的好例子和解释。不过,这是我的问题:为什么编译器无法确定调用模板的位置,并在编译定义文件之前先编译这些文件?我可以想象它可以在一个简单的情况下完成......相互依赖会很快搞乱订单的答案吗?
D
DevSolar

实际上,在 C++11 之前,该标准定义了 export 关键字,使得可以在头文件中声明模板并在其他地方实现它们。以某种说法。并非如此,作为唯一实施该功能 pointed out 的人:

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

幻象优势#2:快速构建,减少依赖。许多用户期望导出将允许将模板真正单独编译为目标代码,他们期望这将允许更快的构建。这不是因为导出模板的编译确实是独立的,但不是目标代码。相反,导出几乎总是使构建变慢,因为至少相同数量的编译工作仍必须在预链接时完成。导出甚至不会减少模板定义之间的依赖关系,因为依赖关系是内在的,独立于文件组织。

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

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


...几年后,我终于明白了 export 究竟会我们什么,什么不是...现在我完全同意EDG 人员:It would not have brought us what most people (myself in '11 included) think it would, and the C++ standard is better off without it.
@DevSolar:这篇论文是政治性的、重复性的、写得不好。那不是通常的标准水平散文。不必要的冗长和无聊,在几十页上基本上说了 3 次相同的事情。但我现在被告知出口不是出口。这是一个很好的情报!
@v.oddou:优秀的开发人员和优秀的技术作家是两个不同的技能组合。有些人可以两者兼得,许多人不能。 ;-)
@v.oddou 这篇论文不仅写得不好,而且是虚假信息。这也是对现实的扭曲:实际上非常强烈的出口论据以某种方式混合在一起,听起来像是反对出口:“在存在出口的情况下发现标准中存在许多与 ODR 相关的漏洞。在导出之前,编译器不必诊断 ODR 违规。现在这是必要的,因为您需要组合来自不同翻译单元的内部数据结构,如果它们实际上代表不同的事物,您就无法组合它们,因此您需要进行检查。”
@DevSolar 我仍然没有在论文中看到反对出口的案例。 (我看到一个出口案例。)
J
Jonny Henly

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

有一个 export 关键字应该可以缓解这个问题,但它离可移植还差得很远。


为什么我不能用关键字“inline”在 .cpp 文件中实现它们?
你可以,而且你甚至不必输入“内联”。但是您只能在该 cpp 文件中使用它们,而不能在其他任何地方使用它们。
这几乎是最准确的答案,除了“这意味着那些编译器不允许在非头文件(如 .cpp 文件)中定义它们”显然是错误的。
C
Cameron Tacklind

模板经常用在头文件中,因为编译器需要实例化不同版本的代码,具体取决于模板参数给定/推导的参数,并且(作为程序员)更容易让编译器重新编译相同的代码多次并在以后进行重复数据删除.请记住,模板并不直接代表代码,而是该代码的多个版本的模板。当您在 .cpp 文件中编译非模板函数时,您正在编译一个具体的函数/类。模板不是这样,它可以用不同的类型实例化,即用具体类型替换模板参数时必须发出具体代码。

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

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

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

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


到目前为止,我知道模块的通用编译器中的已知实现。
l
lafrecciablu

尽管上面有很多很好的解释,但我还是缺少一种将模板分成标题和正文的实用方法。当我更改其定义时,我主要关心的是避免重新编译所有模板用户。在模板主体中包含所有模板实例对我来说不是一个可行的解决方案,因为模板作者可能不知道所有模板的用法,并且模板用户可能无权修改它。我采用了以下方法,它也适用于较旧的编译器(gcc 4.3.4,aCC A.03.13)。

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

示意图示例:

我的模板.h:

#ifndef MyTemplate_h
#define MyTemplate_h 1

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

#endif

我的模板.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 >;

主.cpp:

#include "MyInstantiatedTemplate.h"

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

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


我喜欢这种方法,除了 MyInstantiatedTemplate.h 文件并添加了 MyInstantiatedTemplate 类型。如果你不使用它,它会更干净一点,恕我直言。查看我对显示此内容的另一个问题的回答:stackoverflow.com/a/41292751/4612476
这需要两全其美。我希望这个答案得到更高的评价!另请参阅上面的链接,以更简洁地实现相同的想法。
E
Evan Teran

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

template < typename ... >
class MyClass
{

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

M
Moshe Rabaev

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

然而,由于模板需要在编译步骤中进行处理,以便为您定义的每个模板实例化生成代码,因此简单地编译与其头文件分开的模板是行不通的,因为它们总是手牵手,原因很简单从字面上看,每个模板实例化都是一个全新的类。在常规类中,您可以将 .h 和 .cpp 分开,因为 .h 是该类的蓝图,而 .cpp 是原始实现,因此可以定期编译和链接任何实现文件,但是使用模板 .h 是如何使用模板的蓝图类的外观不应是对象的外观,这意味着模板 .cpp 文件不是类的原始常规实现,它只是类的蓝图,因此无法编译 .h 模板文件的任何实现,因为你需要一些具体的东西来编译,模板在这个意义上是抽象的。

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

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

意思是在编译步骤而不是链接步骤中替换了 typename T创建是因为它不知道 T 是什么。

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

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


K
KeyC0de

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

我的队列.hpp:

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

    bool isEmpty();

    ...
}    

我的队列.cpp:

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


main()
{
    QueueA<char> Q;

    ...
}

对于真正的人???如果这是真的那么你的答案应该被检查为正确的答案。如果你可以在.cpp中定义非模板成员方法,为什么有人需要所有那些hacky voodo的东西?
好吧,那是行不通的。至少在 MSVC 2019 上,为模板类的成员函数获取未解析的外部符号。
我没有要测试的 MSVC 2019。这是 C++ 标准允许的。现在,MSVC 因不总是遵守规则而臭名昭著。如果您还没有,请尝试项目设置 -> C/C++ -> 语言 -> 一致性模式 -> 是(许可)。
这个确切的示例有效,但是您不能从除 myQueue.cpp 之外的任何其他翻译单元调用 isEmpty ...
因此,这对于将庞大的函数移动到 .cpp 文件并声明它们是私有的,而公共函数保留在头文件中并调用它们是一个很好的策略。
E
Eric Shaw

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


这个响应应该被修改得更多。我“独立地”发现了您的相同方法,并专门寻找其他人已经使用过它,因为我很好奇它是否是 官方模式以及它是否有姓名。我的方法是在需要实现 template class X 的地方实现 class XBase,将依赖类型的部分放在 X 中,其余部分放在 XBase 中。
F
Flexo

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


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

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

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


s/inner_foo/foo/g 并在 foo.h 的末尾包含 foo.tpp。少一份文件。
user246672 有一点错误——只需将 .tpp 文件(我使用 .ft)包含在需要它们的 .cpp 文件中。
J
Juan

我建议查看这个 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

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


C
ClarHandsome

在头文件中编写声明和定义是个好主意的另一个原因是为了便于阅读。假设 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,尽管编译器会在此告诉您关于需要覆盖哪个运算符的情况。


K
Kode

您实际上可以在 .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];
    }

大多数人会将头文件定义为将定义传播到源文件的任何内容。因此,您可能已经决定使用文件扩展名“.template”,但您已经编写了一个头文件。