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

Why can templates only be implemented in the header file?

提问人:MainID 提问时间:1/30/2009 最后编辑:πάντα ῥεῖMainID 更新时间:8/26/2023 访问量:771847

问:

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

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

为什么会这样?

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

C 模板 未定义引用 C++-常见问题

评论

29赞 AnT stands with Russia 9/18/2014
虽然将所有模板函数定义放入头文件中可能是使用它们的最便捷方法,但仍然不清楚该引号中的“内联”作用。无需为此使用内联函数。“内联”与此完全无关。
17赞 dgrat 5/28/2019
模板不像可以编译成字节码的函数。它只是生成此类函数的一种模式。如果将模板单独放入 *.cpp 文件中,则无需编译任何内容。此外,显式实例化实际上不是模板,而是从模板中生成函数的起点,该函数最终出现在 *.obj 文件中。
4赞 Vorac 2/14/2022
@AnT也许它们的意思是“内联”不是作为关键字,而是“在声明位置,在类内部实现的方法”。
0赞 palapapa 4/22/2023
@AnTstandswithRussia 模板函数是隐式内联的
1赞 Davis Herring 7/21/2023
@palapapa:这听起来像是另一个问题(或者可能是两个问题,如果不是“这什么时候重要?”而是“这意味着什么?”)。

答:

42赞 Anton Gogolev 1/30/2009 #1

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

有一个导出关键字应该可以缓解这个问题,但它远未达到可移植性。

评论

0赞 MainID 1/30/2009
为什么我不能在带有关键字“内联”的 .cpp 文件中实现它们?
3赞 vava 1/30/2009
你可以,而且你甚至不必把“内联”放在里面。但是您只能在该 cpp 文件中使用它们,而不能在其他地方使用它们。
13赞 Lightness Races in Orbit 8/15/2011
这几乎是最准确的答案,除了“这意味着这些编译器不允许在非头文件(如 .cpp 文件)中定义它们”显然是错误的。
95赞 David Hanak 1/30/2009 #2

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

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

评论

1赞 vava 1/30/2009
“导出”是标准的,但它很难实现,所以大多数编译器团队还没有这样做。
6赞 Pieter 1/30/2009
导出并不能消除源代码披露的需要,也不会减少编译依赖性,但它需要编译器构建者付出巨大的努力。因此,Herb Sutter 本人要求编译器构建者“忘记”导出。由于所需的时间投资最好花在其他地方......
2赞 Pieter 1/30/2009
所以我不认为导出“还没有”实现。除了EDG之外,其他人可能永远不会完成它,因为其他人看到了它花了多长时间,以及收获了多少
3赞 Pieter 1/30/2009
如果你对此感兴趣,这篇论文叫做“为什么我们负担不起出口”,它列在他的博客上(gotw.ca/publications),但那里没有pdf(不过快速谷歌应该会打开它)
1赞 Vlad 10/12/2013
好的,谢谢你的好例子和解释。不过,我的问题是:为什么编译器无法弄清楚在哪里调用模板,并在编译定义文件之前先编译这些文件?我可以想象它可以在一个简单的案例中完成......答案是相互依赖性会很快扰乱秩序吗?
1979赞 Luc Touraille 1/30/2009 #3

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

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

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)中实现该类,并将此实现文件包含在标头的末尾。

Foo.h的

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

#include "Foo.tpp"

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

评论

122赞 Mankarse 5/28/2011
实际上,显式实例化需要位于 .cpp 文件中,该文件可以访问 Foo 所有成员函数的定义,而不是在标头中。
19赞 xcrypt 1/15/2012
“编译器需要能够访问方法的实现,以使用模板参数(在本例中为 int)实例化它们。如果这些实现不在标头中,则无法访问它们“ 但是,为什么编译器无法访问 .cpp 文件中的实现?编译器还可以访问 .cpp 信息,否则它如何将它们转换为 .obj 文件?编辑:这个问题的答案在此答案中提供的链接中...
46赞 zinking 8/23/2012
我不认为这清楚地解释了这个问题,关键的事情显然与本文中没有提到的编译单元有关
7赞 Luc Touraille 2/21/2014
@Gabson:结构和类是等价的,不同之处在于类的默认访问修饰符是“private”,而结构的默认访问修饰符是 public。通过查看此问题,您可以了解其他一些微小的差异。
5赞 Aaron McDaid 8/5/2015
我在这个答案的开头加了一句话,以澄清这个问题是基于一个错误的前提。如果有人问“为什么X是真的?”而事实上X不是真的,我们应该迅速拒绝这个假设。
18赞 Benoît 1/30/2009 #4

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

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
76赞 DevSolar 1/30/2009 #5

实际上,在 C++11 之前,该标准定义了一个关键字,该关键字可以在头文件中声明模板并在其他地方实现它们。在某种程度上。并非如此,正如唯一实现该功能的人所指出的那样:export

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

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

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

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

评论

9赞 DevSolar 11/19/2015
...几年后,我终于明白了什么实际上会给我们什么不会......现在我全心全意地同意EDG人的观点:它不会给我们带来大多数人(包括11年的我自己)所认为的那样,如果没有它,C++标准会更好。export
6赞 v.oddou 4/25/2016
@DevSolar:这篇论文是政治性的、重复的、写得不好的。这不是那里通常的标准水平散文。冗长而无聊,在几十页的篇幅里,基本上说了3次同样的话。但我现在被告知,出口不是出口。这是一个很好的情报!
1赞 DevSolar 4/25/2016
@v.oddou:优秀的开发人员和优秀的技术作家是两个不同的技能组合。有些人可以两者兼而有之,许多人不能。;-)
1赞 curiousguy 12/14/2019
@v.oddou 这篇论文不仅写得不好,而且是虚假信息。此外,这也是对现实的扭曲:实际上支持出口的极其有力的论据在某种程度上混合在一起,使其听起来像是反对出口:“在存在出口的情况下,在标准中发现了许多与ODR相关的漏洞。在导出之前,编译器不必诊断 ODR 违规。现在这是必要的,因为你需要组合来自不同翻译单元的内部数据结构,如果它们实际上代表不同的东西,你就不能组合它们,所以你需要进行检查。
1赞 curiousguy 12/19/2019
@DevSolar我仍然没有看到报纸上反对出口的案例。(我看到一个出口的案例。
276赞 MaHuJa 8/13/2009 #6

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

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

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

template class vector<int>;

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

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

评论

80赞 Jiminion 7/18/2014
呸。很好的答案,但没有真正干净的解决方案。列出模板的所有可能类型似乎与模板应该是什么不符。
10赞 Tomáš Zato 12/9/2014
这在许多情况下可能很好,但通常会破坏模板的目的,该模板旨在允许您在不手动列出它们的情况下将类与任何类一起使用。type
12赞 UncleZeiv 6/3/2015
vector不是一个很好的例子,因为容器本质上是面向“所有”类型的。但是,您创建的模板确实经常用于一组特定的类型,例如数字类型:int8_t、int16_t、int32_t、uint8_t、uint16_t等。在这种情况下,使用模板仍然有意义,但为整个类型集显式实例化它们也是可能的,在我看来,建议这样做。
0赞 Vitt Volt 2/16/2017
在定义模板后使用,“并且所有成员函数都已定义”。谢谢!
1赞 oarfish 9/4/2019
我觉得我错过了什么......我将两种类型的显式实例化放入类的文件中,并且从其他文件中引用这两个实例化,但我仍然收到未找到成员的链接错误。.cpp.cpp
7赞 Robert 9/17/2011 #7

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

评论

7赞 Flexo 9/17/2011
“头文件未编译”——这是一种非常奇怪的描述方式。头文件可以是翻译单元的一部分,就像“c/cpp”文件一样。
4赞 xaxxon 12/22/2015
事实上,这几乎与事实相反,即头文件经常被编译多次,而源文件通常被编译一次。
349赞 Ben 5/11/2013 #8

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

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

  • foo.h
    • 声明class MyClass<T>
  • foo.cpp
    • 定义了class MyClass<T>
  • 条.cpp
    • 使用MyClass<int>

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

当我编译 foo.cpp 时,bar.cpp 甚至不需要存在,但我仍然应该能够将我已经拥有的 foo.o 与我刚刚生成的 bar.o 链接在一起,而无需重新编译 foo.cppfoo.cpp甚至可以被编译成一个动态库,在没有foo.cpp的情况下分发到其他地方,并与我在我编写foo.cpp多年后编写的代码链接。

“实例化样式多态性”意味着模板实际上不是一个泛型类,可以编译为可以处理任何值的代码。这将增加开销,例如装箱、需要将函数指针传递给分配器和构造函数等。C++ 模板的目的是避免编写几乎相同的 、 等,但仍然能够最终得到编译的代码,就好像我们单独编写了每个版本一样。因此,模板实际上就是一个模板;类模板不是一个类,它是为我们遇到的每个类创建一个新类的秘诀。模板不能编译成代码,只能编译实例化模板的结果。MyClass<T>Tclass MyClass_intclass MyClass_floatT

因此,当编译 foo.cpp 时,编译器无法看到 bar.cpp 来知道需要这样做。它可以看到模板,但不能为此发出代码(它是一个模板,而不是一个类)。当 bar.cpp 被编译时,编译器可以看到它需要创建一个 ,但它看不到模板(只有它在 foo.h 中的接口),所以它不能创建它。MyClass<int>MyClass<T>MyClass<int>MyClass<T>

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

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

  • baz.cpp
    • 声明和实现,并使用class BazPrivateMyClass<BazPrivate>

除非我们

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

没有人喜欢(1),因为全程序分析编译系统需要很长时间才能编译,并且因为它使得没有源代码就无法分发编译库。所以我们有 (2) 代替。

评论

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

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

关键字有一个功能,旨在用于单独编译。 该功能在 AFAIK 中已弃用,只有一个编译器实现了它。 您不应该使用 . 单独编译是不可能的,但也许在 中,如果概念进入,我们可以有某种单独编译的方法。exportexportC++11exportC++C++11C++17

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

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

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

评论

0赞 chadmc 5/7/2021
到目前为止,我知道模块的常见编译器中的实现。
27赞 lafrecciablu 5/12/2016 #10

尽管上面有很多很好的解释,但我缺少一种将模板分为标题和正文的实用方法。

我主要关心的是,当我更改其定义时,避免重新编译所有模板用户。

在模板正文中包含所有模板实例对我来说不是一个可行的解决方案,因为模板作者可能不知道它的用法是否全部,模板用户可能无权修改它。

我采用了以下方法,这种方法也适用于较旧的编译器(gcc 4.3.4,aCC A.03.13)。

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

模板的每个用户都包含该头文件并使用 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 >;

main.cpp:

#include "MyInstantiatedTemplate.h"

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

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

评论

2赞 Cameron Tacklind 2/16/2019
我喜欢这种方法,除了文件和添加的类型。恕我直言,如果你不使用它,它会更干净一些。看看我对另一个问题的回答:stackoverflow.com/a/41292751/4612476MyInstantiatedTemplate.hMyInstantiatedTemplate
0赞 Wormer 10/31/2019
这需要两全其美。我希望这个答案能得到更高的评价!另请参阅上面的链接,了解相同想法的更简洁的实现。
12赞 abc 7/19/2016 #11

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

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

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

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

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

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

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

10赞 Eric Shaw 7/27/2016 #12

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

评论

2赞 Fabio A. 11/4/2016
这个响应应该被修改得更多。我“独立”发现了你同样的方法,并专门寻找其他人已经使用过它,因为我很好奇它是否是官方模式以及它是否有名称。我的方法是在我需要实现的地方实现一个,将与类型相关的部分放在里面,其余的都放在里面。class XBasetemplate class XXXBase
8赞 Pranay 5/13/2017 #13

单独实现的方法如下。

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.h具有正向声明。 有实施,包括;并且只有一行,包括 .foo.tppinner_foo.hfoo.hfoo.tpp

在编译时,将 的内容复制到其中,然后将整个文件复制到其中,然后进行编译。这样,就没有限制,并且命名是一致的,以换取一个额外的文件。foo.hfoo.tppfoo.h

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

评论

5赞 1/23/2019
s/inner_foo/foo/g 并在 foo.h 末尾包含 foo.tpp。少一个文件。
0赞 Spencer 3/30/2022
user246672 有点错误——只需将文件(我使用)包含在需要它们的文件中即可。.tpp.ft.cpp
11赞 KeyC0de 7/19/2018 #14

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


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;

    ...
}

评论

3赞 Michael IV 2/19/2020
对于真正的男人???如果这是真的,那么你的答案应该被检查为正确的答案。如果你可以在.cpp中定义非模板成员方法,为什么有人需要所有这些黑客voodo的东西?
0赞 Michael IV 2/21/2020
好吧,那行不通。至少在 MSVC 2019 上,为模板类的成员函数获取未解析的外部符号。
0赞 KeyC0de 2/21/2020
我没有要测试的 MSVC 2019。这是 C++ 标准允许的。现在,MSVC 因不始终遵守规则而臭名昭著。如果尚未尝试,请尝试“项目设置”->“C/C++”-“>语言”-“>一致性模式”->“是(允许)”。
3赞 M.M 4/11/2020
这个确切的例子是有效的,但是你不能从任何其他翻译单元调用......isEmptymyQueue.cpp
0赞 Abhinav Gauniyal 11/8/2020
因此,这可能是将笨重的函数移动到 .cpp 文件并将其声明为私有,而公共函数保留在头文件中并调用它们的好方法。
-12赞 Kode 3/12/2019 #15

我必须编写一个模板类,这个例子对我有用

下面是动态数组类的示例。

#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];
    }

评论

12赞 Tommy 8/28/2019
大多数人会将头文件定义为将定义传播到源文件的任何内容。因此,您可能已经决定使用文件扩展名“.template”,但您已经编写了一个头文件。
-5赞 ClarHandsome 3/20/2019 #16

在头文件中同时编写声明和定义的另一个原因是可读性。假设 Utility.h 中有这样一个模板函数:

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

在实用程序.cpp中:

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

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

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

3赞 Juan 2/18/2020 #17

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

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

0赞 Arnab Das 4/25/2023 #18

Moshe 的回答来自:https://stackoverflow.com/a/38448106/6459849

我这边的一个小贡献,有一个扩展的例子。假设有一个整体的 OperationSuccess,它包含一个 ResponseSuccess,其中包含一个泛型类型。

响应成功.h

template <class T>
class ResponseSuccess {
public:
    ResponseSuccess(const ResponseStatus responseStatus, const T& data) :
        m_responseStatus(responseStatus),
        m_data(data) {}

    ~ResponseSuccess() = default;

    // Basis requirement, have Copy/Move constructor/delete assignment operator

    ResponseStatus getResponseStatus() const {
        return m_responseStatus;
    }

    T getData() const {
        return m_data;
    };

private:
    ResponseStatus m_responseStatus;

    T m_data;
};

操作成功.h

template <class T>
class OperationResponse {
public:
    explicit OperationResponse(ResponseSuccess<T> responseSuccess) :
        m_responseSuccess(std::move(responseSuccess)) {}

    ~OperationResponse() = default;
    // Basis requirement, have Copy/Move constructor/delete assignment operator

    ResponseSuccess<T> getResponseSuccess() const {
        return m_responseSuccess;
    }

private:
    ResponseSuccess<T> m_responseSuccess;
    // have a failure, in case required
};

用法:

MyObject myObj(<ctor_args>);
    ResponseSuccess<MyObject> responseSuccess(ResponseStatus::SUCCESS, myObj);
    OperationResponse<MyObject> successOperationResponse(responseSuccess);
..
// Fetches the response -> successOperationResponse.getResponseSuccess();
2赞 v010dya 7/22/2023 #19

(从封闭的副本复制此处)

我更喜欢将我的所有函数都放在文件中,无论它们是模板函数还是常规函数。有一种方法可以用一些基本的魔法来做到这一点。您可以采取以下措施:.cpp#ifndef

main.cpp

#include "myclass.hpp"

int main()
{
  // ...
}

myclass.hpp

#ifndef MYCLASS
#define MYCLASS

template<class T>
class MyClass
{
  T val;
public:
  MyClass(T val_);
}

#define MYCLASS_FUNCTIONS
#include "myclass.cpp"

#endif

我的类.cpp

#ifndef MYCLASS_FUNCTIONS
#include "myclass.hpp"

// regular functions:
// ...

#else
 
// template functions:
template<class T>
MyClass<T>::MyClass(T val_)
    :val(val_)
{}

// ...
#endif

以下是预编译器如何看待它。我们有两个文件。.cpp

  1. 当我们编译 main.cpp 时,我们:
  2. 包括myclass.hpp
  3. 检查未定义,它是MYCLASS
  4. 定义它
  5. 为编译器提供生成的类的定义(来自模板类)
  6. 包括myclass.cpp
  7. 定义MYCLASS_FUNCTIONS
  8. 检查是否已定义,它是MYCLASS_FUNCTIONS
  9. 为编译器提供生成函数的定义(来自模板函数)
  10. 当我们编译 myclass.cpp
  11. 检查是否已定义,否则未定义MYCLASS_FUNCTIONS
  12. 包括myclass.hpp
  13. 检查未定义,它是MYCLASS
  14. 定义它
  15. 为编译器提供类的定义
  16. 包括myclass.cpp
  17. 再次包含myclass.hpp
  18. 这个时间是定义的,所以什么都不做,回到MYCLASSmyclass.cpp
  19. 检查是否已定义,它是MYCLASS_FUNCTIONS
  20. 为编译器提供生成函数的定义(来自模板函数)
  21. 退出包含两次
  22. 将所有常规函数传递给编译器