协慌网

登录 贡献 社区

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

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

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

为什么是这样?

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

答案

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

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

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

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

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

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

template class vector<int>;

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

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

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

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

  • foo.h 中
    • 声明class MyClass<T>的接口
  • Foo.cpp 中
    • 定义了class MyClass<T>
  • bar.cpp
    • 使用MyClass<int>

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

当我编译foo.cpp 时bar.cpp甚至不需要存在,但是我仍然可以链接foo.o我已经和bar一起了。我刚刚制作了,而不需要重新编译foo .cppfoo.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)。