Answers:
警告:这是没有必要把落实在头文件中,看到这个答案的末尾替代解决方案。
无论如何,您的代码失败的原因是,在实例化模板时,编译器会使用给定的template参数创建一个新类。例如:
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)中实现该类,并在头末尾包含此实现文件。
oo
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
这样,实现仍与声明分开,但编译器可以访问。
另一个解决方案是使实现分离,并显式实例化您需要的所有模板实例:
oo
// 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>;
这将实例化该类及其所有成员函数(仅对链接器可用)。类似的语法适用于模板函数,因此,如果您有非成员运算符重载,则可能需要对它们执行相同的操作。
上面的示例是毫无用处的,因为矢量完全在头文件中定义,除非使用通用的包含文件(预编译的头文件?)extern template class vector<int>
以防止在所有其他使用矢量文件的文件(1000个)中实例化它。
type
而无需手动列出它们。
vector
这不是一个好例子,因为容器本质上是针对“所有”类型的。但是,创建模板仅用于特定类型集的情况确实经常发生,例如数字类型:int8_t,int16_t,int32_t,uint8_t,uint16_t等。在这种情况下,使用模板仍然有意义,但也可以为整个类型的集合显式实例化它们,我认为建议这样做。
.cpp
文件中,并且这两个实例化是从其他.cpp
文件中引用的,但仍然收到未找到成员的链接错误。
这是因为需要单独编译,并且模板是实例化样式的多态性。
让我们更接近具体的解释。说我有以下文件:
class MyClass<T>
class MyClass<T>
MyClass<int>
独立汇编的手段,我应该能够编写Foo.cpp中独立地bar.cpp。编译器完全独立地在每个编译单元上进行分析,优化和代码生成的所有艰苦工作;我们不需要进行整个程序分析。只是链接程序需要立即处理整个程序,因此链接程序的工作实际上要容易得多。
bar.cpp甚至不需要当我编译存在Foo.cpp中,但我仍然能够链接foo.o的我已经与有共同文件bar.o我刚刚产生,而无需重新编译FOO .cpp。甚至可以将foo.cpp编译成动态库,而无需foo.cpp即可将其分发到其他地方,并与他们在我编写foo.cpp之后数年编写的代码链接。
“实例化样式多态性”表示模板MyClass<T>
并不是真正的泛型类,可以将其编译为可用于任何值的代码T
。这会增加开销,如拳击,需要对函数指针传递给分配器和构造等的C ++模板的目的是为了避免写几乎相同class MyClass_int
,class 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的作者认为提供的模板化类的现有版本。
您可能会认为,在编译模板时,编译器应“生成所有版本”,并且在链接过程中会滤除从未使用过的版本。除了庞大的开销和极端的困难之外,这种方法还会面临困难,因为指针和数组之类的“类型修饰符”功能甚至允许内置类型产生无限数量的类型,当我现在扩展程序时会发生什么通过添加:
class BazPrivate
,并使用MyClass<BazPrivate>
除非我们要么
MyClass<T>
MyClass<T>
,以便编译器可以MyClass<BazPrivate>
在baz.cpp的编译期间生成。没有人喜欢(1),由于整个程序分析的编译系统采取永远编译,因为它使得它不可能没有源代码分发编译库。因此,我们改为(2)。
在实际将模板编译为目标代码之前,需要由编译器实例化模板。仅在知道模板参数的情况下才能实现此实例化。现在,设想一种在中声明a.h
,定义a.cpp
和使用模板函数的情况b.cpp
。当a.cpp
被编译,它不一定知道,即将到来的编纂b.cpp
将需要模板的实例,更何况哪个特定实例会是这样。对于更多的头文件和源文件,情况可能很快变得更加复杂。
可以争论的是,可以使编译器变得更聪明,以“预见”模板的所有用途,但是我敢肯定,创建递归或其他复杂的场景并不困难。AFAIK,编译器不会提前这样做。正如Anton所指出的,一些编译器支持模板实例的显式导出声明,但并非所有编译器都支持(实例?)。
实际上,之前C ++ 11标准中定义的export
关键字将使其能够在头文件中声明的模板和其他地区实施。
没有一个流行的编译器实现此关键字。我唯一了解的是Edison Design Group编写的前端,它由Comeau C ++编译器使用。其他所有程序都要求您在头文件中编写模板,因为编译器需要模板定义以进行正确的实例化(正如其他人已经指出的那样)。
结果,ISO C ++标准委员会决定删除export
具有C ++ 11的模板的功能。
export
实际上会给我们带来什么,而没有给我们带来什么...现在我全心全意地同意EDG的想法
尽管标准C ++没有这样的要求,但是某些编译器要求所有函数和类模板都必须在使用的每个翻译单元中都可用。实际上,对于那些编译器,必须在头文件中提供模板函数的主体。重复一遍:这意味着那些编译器不允许在非头文件(例如.cpp文件)中定义它们
有一个export关键字可以缓解此问题,但是它几乎没有可移植性。
必须在标头中使用模板,因为编译器需要根据给定/推导的模板参数实例化不同版本的代码。请记住,模板并不直接代表代码,而是代表该代码的多个版本的模板。当您在.cpp
文件中编译非模板函数时,您正在编译具体的函数/类。模板不是这种情况,可以用不同的类型实例化模板,即,用具体类型替换模板参数时必须发出具体代码。
export
关键字具有一个功能,该功能旨在用于单独的编译。该export
功能已在C++11
AFAIK中弃用,只有一个编译器实现了该功能。您不应该使用export
。在C++
或中C++11
可能无法进行单独编译C++17
,如果有概念可以实现,则我们可以采用某种方式进行单独编译。
为了实现单独的编译,必须可以进行单独的模板主体检查。似乎可以用概念来解决。看一下最近在标准委员会会议上发表的这篇论文。我认为这不是唯一的要求,因为您仍然需要为用户代码中的模板代码实例化代码。
模板的单独编译问题我想这也是当前正在工作的向模块迁移的问题。
尽管上面有很多很好的解释,但我仍然缺少将模板分为标题和正文的实用方法。
我主要关心的是在更改模板定义时避免重新编译所有模板用户。
对我而言,在模板主体中包含所有模板实例化不是一个可行的解决方案,因为模板创建者可能不了解其用法,并且模板用户可能无权对其进行修改。
我采用了以下方法,该方法也适用于较早的编译器(gcc 4.3.4,aCC A.03.13)。
对于每种模板用法,其自己的头文件中都有一个typedef(从UML模型生成)。它的主体包含实例化(实例化最终在一个链接到最后的库中)。
模板的每个用户都包括该头文件并使用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
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;
}
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;
}
这样,只需要重新编译模板实例,而不是所有模板用户(和依赖项)。
MyInstantiatedTemplate.h
文件和添加的MyInstantiatedTemplate
类型外,我喜欢这种方法。恕我直言,如果您不使用它,那会更清洁。在另一个显示此问题的问题上签出我的答案:stackoverflow.com/a/41292751/4612476
只是在这里添加一些值得注意的东西。当它们不是函数模板时,可以在实现文件中很好地定义模板化类的方法。
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;
...
}
isEmpty
从myQueue.cpp
... 之外的任何其他翻译部门打电话
在编译步骤中使用模板时,编译器将为每个模板实例生成代码。在编译和链接过程中,.cpp文件将转换为包含引用或未定义符号的纯目标代码或机器代码,因为main.cpp中包含的.h文件没有实现。它们准备好与定义模板实现的另一个目标文件链接,因此您具有完整的a.out可执行文件。
但是,由于模板需要在编译步骤中进行处理才能为您定义的每个模板实例生成代码,因此仅将模板与其头文件分开编译就无法正常工作,因为它们总是并存的从字面上看,每个模板实例都是一个全新的类。在常规类中,您可以将.h和.cpp分开,因为.h是该类的蓝图,而.cpp是原始实现,因此任何实现文件都可以定期进行编译和链接,但是使用模板.h是如何实现的蓝图类应该看起来不像对象看起来的样子,意味着.cpp模板文件不是类的原始常规实现,它只是类的蓝图,因此任何.h模板文件的实现都可以。
因此,模板永远不会单独编译,仅在其他源文件中有具体实例的任何地方编译。但是,具体的实例化需要知道模板文件的实现,因为只需修改typename T
在.h文件中使用具体类型将无法完成工作,因为那里有.cpp链接,我以后找不到它,因为记住模板是抽象的并且无法编译,所以我被迫现在就给出实现,这样我就知道要编译和链接的内容了,现在有了实现,它就链接到了封闭的源文件中。基本上,实例化模板的那一刻,我需要创建一个全新的类,并且如果不知道使用所提供的类型时该类的外观,则无法这样做,除非我注意到编译器模板实现,因此现在编译器可以替换T
为我的类型,并创建一个准备好进行编译和链接的具体类。
综上所述,模板是类外观的蓝图,类是对象外观的蓝图。我无法将模板与具体实例分开进行编译,因为编译器仅编译具体类型,换句话说,至少在C ++中,模板是纯语言抽象。可以这么说,我们必须取消模板的抽象,我们通过为模板提供要处理的具体类型,以便模板抽象可以转换为常规的类文件,然后可以正常地对其进行编译。分隔模板.h文件和模板.cpp文件是没有意义的。这是荒谬的,因为仅.cpp和.h的分隔仅是.cpp可以单独编译和分别与模板链接的地方,因为我们不能单独编译它们,因为模板是抽象的,
意思typename T
是在编译步骤而不是链接步骤中替换了get,因此,如果我尝试编译模板而不将T
其替换为对编译器完全没有意义的具体值类型,则由于无法创建目标代码,因此无法创建该模板知道是什么T
。
从技术上讲,可以创建某种功能来保存template.cpp文件,并在其他来源中找到它们时切换出类型,我认为该标准的确有一个关键字export
,可以将模板放在一个单独的位置cpp文件,但实际上并没有很多编译器实现。
附带说明一下,在对模板类进行专业化处理时,可以将标头与实现分开,因为按定义进行的专业化处理意味着我专门针对可以单独编译和链接的具体类型。
单独实施的方法如下。
//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具有前向声明。foo.tpp具有实现,并包含inner_foo.h;。和foo.h只有一行,包括foo.tpp。
在编译时,将foo.h的内容复制到foo.tpp,然后将整个文件复制到foo.h,然后进行编译。这样,就没有限制,并且命名是一致的,以换取一个额外的文件。
我这样做是因为当在* .tpp中看不到类的前向声明时,代码的静态分析器会中断。在任何IDE中编写代码或使用YouCompleteMe或其他代码时,这很烦人。
我建议看一下这个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
这在许多编译器中都得到了支持。但是,我认为模板头文件很难预编译头。
在头文件中同时写入声明和定义的一个好主意是出于可读性。假设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,尽管编译器会在此告诉您。有关需要重写哪个运算符的案例。
您实际上可以在.template文件而不是.cpp文件中定义模板类。无论谁说您只能在头文件中定义它都是错误的。这可以一直追溯到c ++ 98。
不要忘记让编译器将.template文件视为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文件中,您可以按通常的方式定义函数。
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];
}