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

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

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

这是为什么?

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

705374 次浏览

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

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

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

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

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

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

template<typename T>struct Foo{T bar;void doSomething(T param) {/* do stuff using T */}};
// somewhere in a .cppFoo<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"

Foo.tpp

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

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

替代解决方案

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

foo. h

// no implementationtemplate <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods
// explicit instantiationstemplate class Foo<int>;template class Foo<float>;// You will only be able to use Foo with int or float

如果我的解释不够清楚,你可以看看C++关于此主题的超级FAQ

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

template < typename ... >class MyClass{
int myMethod(){// Not just declaration. Add method implementation here}};

实际上,在C++11之前,标准定义了export关键字,使在头文件中声明模板并在其他地方实现它们成为可能。在某种程度上说。不是真的,作为唯一拥有实施特征指出的人:

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

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

没有一个流行的编译器实现了这个关键字。该特性的唯一实现是在爱迪生设计小组编写的前端中,ComeauC++编译器使用它。所有其他编译器都要求您在头文件中编写模板,因为编译器需要模板定义才能正确实例化(正如其他人已经指出的那样)。

因此,ISOC++标准委员会决定删除带有C++11的模板的export功能。

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

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

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

template class vector<int>;

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

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

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

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

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

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

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

当我编译foo.cpp时,bar.cpp甚至不需要存在,但我仍然可以将我已经拥有的foo与我刚刚生成的bar. o链接在一起,而无需重新编译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时,它们可以连接起来并正常工作。我们可以利用这一事实通过编写单个模板来允许在. 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)代替。

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

有一个带有export关键字的功能,用于单独编译。export特性在C++11中被弃用,AFAIK中只有一个编译器实现了它。你不应该使用export。单独编译在C++C++11中是不可能的,但也许在C++17中,如果概念进入,我们可以有一些单独编译的方式。

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

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

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

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

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

在模板主体中拥有所有模板实例化对我来说不是一个可行的解决方案,因为模板作者可能不知道它的用法,模板用户可能没有权利修改它。

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

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

模板的每个用户都包含该头文件并使用typedef。

一个示意图示例:

我的模板

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

我的实例化模板. 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;}

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

当您在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。在编译和链接过程中,. 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替换为对编译器完全没有意义的具体值类型,结果无法创建目标代码,因为它不知道T是什么。

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

只是一个旁注,在为模板类进行专业化时,您可以将标头与实现分开,因为专业化定义意味着我正在为可以单独编译和链接的具体类型进行专业化。

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

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

inner_fooH

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.tpp有实现并包括inner_foo.hfoo.h只有一行,包括foo.tpp

在编译时,foo.h的内容被复制到foo.tpp,然后整个文件被复制到foo.h,然后它编译。这样,没有限制,命名是一致的,以换取一个额外的文件。

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

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


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;
...}

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

不要忘记让你的编译器把你的模板文件当作一个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 <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 arrayT* 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];}

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

我建议查看这个gcc页面,它讨论了模板实例化的“cFront”和“borland”模型之间的权衡。

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

“borland”模型符合作者的建议,提供完整的模板定义,并多次编译。

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

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

这是我的方法,因为我提供了一个工作程序,而不是用于其他程序的模板库。这本书的作者Josuttis在模板库方面做了很多工作。

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

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