引自The C++ standard library: a tutorial and handbook:
目前使用模板的唯一可移植方法是使用内联函数在头文件中实现它们。
为什么会这样?
(澄清:标题文件不是唯一的便携式解决方案。但它们是最方便的便携式解决方案。)
答案 0 :(得分:1374)
将实现放在头文件中是不,请参阅本答案末尾的替代解决方案。
无论如何,代码失败的原因是,在实例化模板时,编译器会使用给定的模板参数创建一个新类。例如:
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),并在头的末尾包含此实现文件。
// 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 on this subject。
答案 1 :(得分:227)
这里有很多正确答案,但我想补充一下(为了完整性):
如果您在实现cpp文件的底部,对模板将使用的所有类型进行显式实例化,链接器将能够照常查找它们。
编辑:添加显式模板实例化的示例。在定义模板后使用,并且已定义所有成员函数。
template class vector<int>;
这将实例化(并因此使链接器可用)类及其所有成员函数(仅)。类似的语法适用于模板函数,因此如果您有非成员运算符重载,则可能需要对它们执行相同的操作。
上面的例子相当无用,因为vector在头文件中完全定义,除非公共包含文件(预编译头文件?)使用extern template class vector<int>
,以防止它在所有 other <中实例化它/ em>(1000?)使用vector的文件。
答案 2 :(得分:210)
这是因为需要单独编译,因为模板是实例化风格的多态性。
让我们更接近具体解释。说我有以下文件:
class MyClass<T>
class MyClass<T>
MyClass<int>
单独编译意味着我应该能够独立于 bar.cpp 编译 foo.cpp 。编译器完全独立地在每个编译单元上完成分析,优化和代码生成的所有艰苦工作;我们不需要进行全程序分析。只有链接器需要立即处理整个程序,链接器的工作要容易得多。
编译 foo.cpp 时,bar.cpp 甚至不需要存在,但我仍然可以链接 foo.o < / strong>我已经和我刚刚制作的 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>
(只有它的接口在< strong> 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>
的完整模板,以便编译器在 baz编译期间生成MyClass<BazPrivate>
的.cpp 即可。没有人喜欢(1),因为整个程序分析编译系统需要永远进行编译,因为它使得在没有源代码的情况下分发编译库成为不可能。所以我们改为(2)。
答案 3 :(得分:71)
在将文件实际编译为目标代码之前,编译器需要实例化模板。只有在模板参数已知的情况下才能实现此实例化。现在假设一个场景,其中模板函数在a.h
中声明,在a.cpp
中定义并在b.cpp
中使用。编译a.cpp
时,不一定知道即将进行的编译b.cpp
将需要模板的实例,更不用说具体的实例。对于更多的头文件和源文件,情况可能会变得更加复杂。
有人可以说,编译器可以变得更聪明,可以“预见”模板的所有用途,但我确信创建递归或其他复杂场景并不困难。 AFAIK,编译器不会这样做。正如Anton所指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(但是?)。
答案 4 :(得分:58)
实际上,在C ++ 11之前,标准定义了export
关键字 可以在头文件中声明模板并在其他地方实现它们。
没有一个流行的编译器实现了这个关键字。我所知道的唯一一个是Edison Design Group编写的前端,它由Comeau C ++编译器使用。所有其他人都要求您在头文件中编写模板,因为编译器需要模板定义才能进行正确的实例化(正如其他人已经指出的那样)。
因此,ISO C ++标准委员会决定使用C ++ 11删除模板的export
功能。
答案 5 :(得分:34)
虽然标准C ++没有这样的要求,但是一些编译器要求所有函数和类模板都需要在它们使用的每个翻译单元中可用。实际上,对于那些编译器,模板函数的主体必须在头文件中可用。重复:这意味着那些编译器不允许在非头文件中定义它们,例如.cpp文件
有一个 export 关键字可以缓解这个问题,但它无法移植。
答案 6 :(得分:27)
必须在头文件中使用模板,因为编译器需要实例化不同版本的代码,具体取决于模板参数的给定/推导参数。请记住,模板不直接代表代码,而是代表该代码的多个版本的模板。
在.cpp
文件中编译非模板函数时,您正在编译具体的函数/类。模板不是这种情况,可以使用不同类型进行实例化,即在用具体类型替换模板参数时必须发出具体代码。
有一个export
关键字的功能,用于单独编译。
export
中不再使用C++11
功能,而AFAIK只有一个编译器实现了它。你不应该使用export
。在C++
或C++11
中无法进行单独编译,但可能在C++17
中,如果概念允许,我们可以采用某种方式进行单独编译。
要实现单独的编译,必须单独进行模板体检查。似乎可以通过概念实现解决方案。看看最近在这里发表的paper 标准委员会会议。我认为这不是唯一的要求,因为您仍然需要在用户代码中实例化模板代码的代码。
模板的单独编译问题我想这也是迁移到模块时出现的一个问题,目前正在运行。
答案 7 :(得分:15)
这意味着定义模板类方法实现的最便携方式是在模板类定义中定义它们。
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
答案 8 :(得分:12)
尽管上面有很多好的解释,但我错过了将模板分成标题和正文的实用方法。
我主要担心的是当我改变其定义时,避免重新编译所有模板用户
模板体中的所有模板实例化对我来说都不是一个可行的解决方案,因为模板作者可能不知道它的用法和模板用户是否有权修改它。
我采用了以下方法,该方法也适用于较旧的编译器(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;
}
这样只需要重新编译模板实例化,而不是所有模板用户(和依赖项)。
答案 9 :(得分:6)
这是完全正确的,因为编译器必须知道它的分配类型。所以模板类,函数,枚举等也必须在头文件中实现,如果它要公开或者是库的一部分(静态或动态),因为头文件不是编译的,不像c / cpp文件那样是。如果编译器不知道类型是无法编译它。在.Net中它可以因为所有对象都派生自Object类。这不是.Net。
答案 10 :(得分:6)
如果关注的是额外的编译时间和二进制大小膨胀是通过将.h编译为使用它的所有.cpp模块的一部分而产生的,在许多情况下,你可以做的是使模板类从非模板化下降接口的非类型相关部分的基类,该基类可以在.cpp文件中实现。
答案 11 :(得分:2)
单独实施的方法如下。
//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或其他代码时,这很烦人。
答案 12 :(得分:2)
只需在此处添加一些值得注意的内容即可。当它们不是函数模板时,可以在实现文件中很好地定义模板类的方法。
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;
...
}
答案 13 :(得分:1)
在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。 在编译和链接过程中.cpp文件被转换为纯对象或机器代码,其中包含引用或未定义的符号,因为main.cpp中包含的.h文件没有实现YET。这些已准备好与另一个定义模板实现的目标文件链接,因此您有一个完整的a.out可执行文件。 但是,由于模板需要在编译步骤中处理,以便为您在主程序中执行的每个模板实例化生成代码,因此链接无济于事,因为将main.cpp编译为main.o然后编译模板.cpp到template.o然后链接将无法实现模板的目的,因为我将不同的模板实例链接到相同的模板实现!模板应该做相反的事情,即具有一个实现,但允许通过使用一个类进行许多可用的实例化。
意思typename T
在编译步骤中取代了而不是链接步骤,所以如果我尝试编译模板而不将T
替换为具体的值类型,那么它将无法工作,因为这是定义模板是一个编译时进程,而btw元编程就是使用这个定义。
答案 14 :(得分:1)
我建议查看此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
这在许多编译器中都得到了支持。但是,我认为模板头文件很难预编译头。
答案 15 :(得分: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,尽管编译器会告诉您在这种情况下,您需要确定需要重写哪个运算符。
答案 16 :(得分:-1)
您实际上可以在.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];
}