首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >为什么模板只能在头文件中实现?

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

Stack Overflow用户
提问于 2009-01-30 10:06:50
回答 17查看 704.7K关注 0票数 2.2K

来自C++标准库:教程和手册的报价

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

为什么会这样呢?

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

EN

回答 17

Stack Overflow用户

回答已采纳

发布于 2009-01-30 10:26:41

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

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

代码语言:javascript
复制
template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

当读取这一行时,编译器将创建一个新类(让我们称之为FooInt),它相当于以下内容:

代码语言:javascript
复制
struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

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

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

Foo.h

代码语言:javascript
复制
template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

代码语言:javascript
复制
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

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

替代解

另一种解决方案是将实现分开,并显式实例化所需的所有模板实例:

Foo.h

代码语言:javascript
复制
// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

代码语言:javascript
复制
// 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++超级常见问题解答

票数 1.9K
EN

Stack Overflow用户

发布于 2013-05-11 03:54:29

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

让我们离混凝土近一点来解释一下。假设我有以下文件:

  • 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.cppE 240。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>的完整模板,以便编译器可以在编译baz.cpp期间生成MyClass<BazPrivate>

没有人喜欢(1),因为完整的程序分析编译系统需要很长时间才能编译,而且因为没有源代码就不可能分发编译好的库。所以我们用(2)代替。

票数 335
EN

Stack Overflow用户

发布于 2009-08-13 13:49:17

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

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

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

代码语言:javascript
复制
template class vector<int>;

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

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

票数 271
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/495021

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档