引用C++标准库:教程和手册:
目前使用模板的唯一可移植方式是使用内联函数在头文件中实现它们。
这是为什么?
(澄清:头文件不是唯一的可移植解决方案。但它们是最方便的可移植方案。)
引用C++标准库:教程和手册:
目前使用模板的唯一可移植方式是使用内联函数在头文件中实现它们。
这是为什么?
(澄清:头文件不是唯一的可移植解决方案。但它们是最方便的可移植方案。)
尽管标准C++没有这样的要求,但一些编译器要求所有函数和类模板都必须在使用的每个翻译单元中可用。实际上,对于这些编译器,模板函数的主体必须在头文件中可用。重复:这意味着这些编译器不允许在.cpp文件等非头文件中定义它们
有一个导出关键字可以缓解这个问题,但它离可移植性还很远。
在实际将模板编译成目标代码之前,需要由编译器实例化模板。只有当模板参数已知时,才能实现此实例化。现在想象一个场景,其中模板函数在a.h中声明,在a.pp中定义,并在b.cpp中使用。编译a.pp时,不一定知道即将到来的编译b.cpp将需要模板的实例,更不用说是哪个特定的实例。对于更多的头文件和源文件,情况可能会很快变得更加复杂。
有人会说,编译器可以更聪明地“前瞻”模板的所有用途,但我确信创建递归或其他复杂场景并不困难。顺便说一句,编译器不会再这样做了。正如Anton所指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(现在?)。
注意:没有必要将实现放在头文件中,请参阅答案末尾的替代解决方案。
无论如何,代码失败的原因是,在实例化模板时,编译器使用给定的模板参数创建一个新类。例如:
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)中实现类,并在头的末尾包含该实现文件。
食品h
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
食品.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
这样,实现仍然与声明分离,但编译器可以访问。
替代解决方案
另一个解决方案是保持实现分离,并显式实例化您需要的所有模板实例:
食品h
// no implementation
template <typename T> struct 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++超级常见问题解答。
这意味着定义模板类的方法实现的最可移植的方式是在模板类定义中定义它们。
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
实际上,在C++11之前,该标准定义了export关键字,它可以在头文件中声明模板并在其他地方实现它们。在某种程度上讲。并非如此,正如唯一实现过该功能的人所指出的:
幻影优势#1:隐藏源代码。许多用户表示,他们希望通过使用导出不再需要为类的成员/非成员函数模板和成员函数提供定义模板。这不是真的。在导出的情况下,库编写者仍然必须提供完整的模板源代码或其直接因为实例化需要完整的信息。[...]
幻影优势#2:快速构建,减少依赖性。许多用户希望导出将允许真正的分离将模板编译为目标代码,他们希望这样可以加快构建速度。这不是因为导出模板的编译确实是独立的,但与目标代码无关。相反,出口几乎总是构建速度较慢,因为至少在预链接时仍必须完成相同数量的编译工作。出口甚至不减少模板定义之间的依赖性,因为依赖性是内在的,独立于文件组织。
没有一个流行的编译器实现了这个关键字。该功能的唯一实现是由Edison Design Group编写的前端,由Comeau C++编译器使用。所有其他人都要求您在头文件中编写模板,因为编译器需要模板定义来进行适当的实例化(正如其他人已经指出的那样)。
因此,ISO C++标准委员会决定删除C++11模板的导出功能。
这里有很多正确的答案,但我想补充一下(为了完整):
如果在实现cpp文件的底部,对模板将使用的所有类型进行显式实例化,链接器将能够像往常一样找到它们。
编辑:添加显式模板实例化示例。在定义模板并定义所有成员函数后使用。
template class vector<int>;
这将实例化(从而使链接器可用)类及其所有成员函数(仅限)。类似的语法适用于函数模板,因此如果您有非成员运算符重载,则可能需要对这些重载执行相同的操作。
上面的例子是非常无用的,因为vector是在头文件中完全定义的,除非公共include文件(预编译头文件?)使用外部模板类vector<int>,以防止它在所有其他使用vector的(1000?)文件中实例化。
这是完全正确的,因为编译器必须知道它的分配类型。模板类、函数、枚举等。。如果要将头文件公开或作为库的一部分(静态或动态),则也必须在头文件中实现,因为头文件不像c/cpp文件那样编译。如果编译器不知道该类型,则无法编译它。在.Net中,它可以编译,因为所有对象都派生自Object类。这不是.Net。
这是因为需要单独编译,并且模板是实例化样式多态性。
让我们更接近具体的解释。假设我有以下文件:
小时声明类MyClass<T>的接口foo.cpp定义类MyClass<T>的实现棒.cpp使用MyClass<int>
独立编译意味着我应该能够独立于bar.cpp编译foo.cpp。编译器完全独立地在每个编译单元上完成分析、优化和代码生成的所有艰苦工作;我们不需要做整个程序分析。只有链接器需要一次处理整个程序,而且链接器的工作要简单得多。
当我编译foo.cpp时,bar.cpp甚至不需要存在,但我应该仍然能够将我已经拥有的foo.o与我刚刚制作的bar.o链接在一起,而不需要重新编译foo.cpp。foo.cpp甚至可以被编译成一个动态库,在没有foo.cpp的情况下分发到其他地方,并与他们在我编写foo.cpp多年后编写的代码链接。
“实例化样式多态性”意味着模板MyClass<T>实际上不是一个通用类,它可以编译为可用于任何T值的代码。这会增加诸如装箱之类的开销,需要将函数指针传递给分配器和构造函数等。C++模板的目的是避免编写几乎相同的类MyClass_int、类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作者认为可以提供的模板类的预先存在的版本。
您可能会认为,在编译模板时,编译器应该“生成所有版本”,在链接过程中过滤掉从未使用过的版本。除了由于指针和数组等“类型修改器”特性,即使是内置类型也可以产生无限数量的类型,因此这种方法将面临巨大的开销和极端困难外,现在我通过添加以下内容来扩展程序时会发生什么:
巴兹卡普声明并实现类BazPrivate,并使用MyClass<BazPrivate>
除非我们
每次我们更改程序中的任何其他文件时,都必须重新编译foo.cpp,以防它添加了MyClass的新实例<T>要求baz.cpp包含(可能通过headerincludes)MyClass<T>的完整模板,以便编译器可以在编译baz.cpp期间生成MyClass<BazPrivate>。
没有人喜欢(1),因为整个程序分析编译系统需要很长时间才能编译,而且如果没有源代码,就无法分发编译后的库。所以我们有(2)。
模板通常用于标头中,因为编译器需要实例化不同版本的代码,这取决于为模板参数给定/推导的参数,并且(作为程序员)更容易让编译器多次重新编译同一代码,然后进行重复数据消除。请记住,模板并不直接表示代码,而是该代码的多个版本的模板。当您在.cpp文件中编译非模板函数时,您正在编译一个具体的函数/类。模板的情况并非如此,它可以用不同的类型实例化,也就是说,当用具体类型替换模板参数时,必须发出具体的代码。
export关键字有一个功能,用于单独编译。导出功能在C++11中被弃用,而且,AFAIK中只有一个编译器实现了它。你不应该利用出口。在C++或C++11中,单独编译是不可能的,但在C++17中,如果概念允许,我们可以有一些单独编译的方法。
为了实现单独的编译,必须可以进行单独的模板体检查。似乎可以用概念来解决问题。看看最近发表在标准委员会会议。我认为这不是唯一的要求,因为您仍然需要为用户代码中的模板代码实例化代码。
模板的单独编译问题我想也是迁移到模块时出现的问题,目前正在进行中。
编辑:截至2020年8月,模块已经成为C++的现实:https://en.cppreference.com/w/cpp/language/modules
尽管上面有很多很好的解释,但我缺少一种将模板分离为页眉和正文的实用方法。
我主要担心的是,当我更改其定义时,避免重新编译所有模板用户。
对我来说,在模板主体中包含所有模板实例不是一个可行的解决方案,因为模板作者可能不知道其使用情况,模板用户可能无权修改它。
我采用了以下方法,这也适用于较旧的编译器(gcc 4.3.4,aCC A.03.13)。
对于每个模板的使用,在其自己的头文件(从UML模型生成)中都有一个typedef。它的主体包含实例化(最终在一个库中,该库在末尾链接)。
模板的每个用户都包含该头文件并使用typedef。
示意图示例:
MyTemplate.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
我的实例化模板.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文件没有实现YET。这些文件已准备好与另一个对象文件链接,该对象文件定义了模板的实现,因此您拥有完整的a.out可执行文件。
然而,由于模板需要在编译步骤中进行处理,以便为您定义的每个模板实例化生成代码,因此简单地编译一个与它的头文件分离的模板是行不通的,因为它们总是并行的,因为每个模板实例化都是一个全新的类。在常规类中,您可以将.h和.cpp分开,因为.h是该类的蓝图,.cpp是原始实现,因此任何实现文件都可以定期编译和链接,但是使用模板.h是类外观的蓝图,而不是对象外观,这意味着模板.cpp文件不是类的原始常规实现,它只是一个类的蓝图,因此任何.h模板文件的实现都无法编译,因为您需要一些具体的东西来编译,在这个意义上,模板是抽象的。
因此,模板永远不会单独编译,只在其他源文件中有具体实例化的地方编译。然而,具体的实例化需要知道模板文件的实现,因为简单地使用.h文件中的具体类型修改类型名T并不能完成任务,因为.cpp要链接什么,我以后找不到它,因为记住模板是抽象的,无法编译,所以我现在不得不给出实现,这样我就知道要编译和链接什么,现在我有了实现,它就链接到了封闭的源文件中。基本上,当我实例化一个模板时,我需要创建一个全新的类,如果我不知道该类在使用我提供的类型时应该是什么样子,我就不能这样做,除非我通知编译器模板实现,所以现在编译器可以用我的类型替换t,并创建一个可以编译和链接的具体类。
总之,模板是类外观的蓝图,类是对象外观的蓝图。我不能将模板与它们的具体实例化分开编译,因为编译器只编译具体类型,换句话说,至少在C++中,模板是纯语言抽象的。可以说,我们必须对模板进行去抽象,我们通过给它们一个具体的类型来处理,这样我们的模板抽象就可以转换为一个常规的类文件,进而可以正常编译。分隔template.h文件和template.cpp文件是没有意义的。这是没有意义的,因为.cpp和.h的分离仅限于.cpp可以单独编译和单独链接的地方,因为我们不能单独编译它们,因为模板是一个抽象,因此,我们总是被迫将抽象与具体实例化放在一起,其中具体实例化总是必须知道所使用的类型。
这意味着typename T get在编译步骤而不是链接步骤中被替换,所以如果我试图编译一个模板而不将T替换为一个对编译器来说完全没有意义的具体值类型,结果无法创建对象代码,因为它不知道T是什么。
从技术上讲,可以创建某种功能来保存template.cpp文件,并在其他源中找到它们时切换类型。
附带说明一下,当为模板类进行专门化时,可以将头与实现分开,因为根据定义,专门化意味着我专门化了一个可以单独编译和链接的具体类型。
如果问题是将.h编译为使用它的所有.cpp模块的一部分所产生的额外编译时间和二进制大小膨胀,那么在许多情况下,您可以做的是使模板类从接口的非类型依赖部分的非模板化基类下降,并且该基类可以在.cpp文件中实现。
单独实现的方法如下。
内部_内部.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
}
小时
#include <foo.tpp>
主.cpp
#include <foo.h>
inner_fo.h具有前向声明。foo.tpp具有实现并包括inner_fo.h;而foo.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;
...
}
我必须写一个模板类,这个例子对我很有用
下面是一个动态数组类的示例。
#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];
}
在头文件中编写声明和定义是一个好主意的另一个原因是为了可读性。假设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
这在许多编译器中得到了支持。然而,我认为使用模板头文件时,预编译头文件会很困难。