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

问题描述 投票:1568回答:16

来自The C++ standard library: a tutorial and handbook的报价:

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

为什么是这样?

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

c++ templates c++-faq
16个回答
1407
投票

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

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

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


6
投票

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


6
投票

如果关注的是额外的编译时间和二进制大小膨胀是通过将.h编译为使用它的所有.cpp模块的一部分而产生的,在许多情况下,你可以做的是使模板类从非模板化的基类下降接口的非类型相关部分,该基类可以在.cpp文件中实现它。


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或其他代码时,这很烦人。


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;

    ...
}

2
投票

在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。在编译和链接过程中.cpp文件被转换为纯对象或机器代码,其中包含引用或未定义的符号,因为main.cpp中包含的.h文件没有实现YET。这些已准备好与另一个定义模板实现的目标文件链接,因此您有一个完整的a.out可执行文件。

但是,由于模板需要在编译步骤中处理,以便为您定义的每个模板实例化生成代码,因此简单地编译与其头文件分开的模板将无法工作,因为它们总是手拉手,因为它们每个模板实例化都是字面上的全新类。在常规类中,您可以分隔.h和.cpp,因为.h是该类的蓝图,而.cpp是原始实现,因此任何实现文件都可以定期编译和链接,但是使用模板.h是如何该类应该看起来不是对象看起来应该是什么意思模板.cpp文件不是类的原始常规实现,它只是一个类的蓝图,因此.h模板文件的任何实现都无法编译,因为你需要具体的东西来编译,模板在这个意义上是抽象的。

因此,模板永远不会单独编译,只能在某些其他源文件中具有具体实例的任何位置进行编译。但是,具体的实例化需要知道模板文件的实现,因为简单地使用.h文件中的具体类型修改typename T是不会做的,因为.cpp是什么链接,我找不到它后来因为记住模板是抽象的而且无法编译,所以我现在不得不给出实现所以我知道要编译和链接的内容,现在我已经实现了它被链接到封闭的源文件中。基本上,我实例化模板的那一刻我需要创建一个全新的类,如果我不知道在使用我提供的类型时该类应该是什么样子我不能这样做,除非我注意到编译器模板实现,所以现在编译器可以用我的类型替换T并创建一个可以编译和链接的具体类。

总而言之,模板是类应该看起来的蓝图,类是对象应该如何看的蓝图。我无法将模板与其具体实例分开编译,因为编译器只编译具体类型,换句话说,至少在C ++中的模板是纯语言抽象。我们必须去抽象模板,我们这样做是通过给它们一个具体的类型来处理,这样我们的模板抽象就可以转换成一个普通的类文件,反过来,它可以正常编译。分离模板.h文件和模板.cpp文件是没有意义的。这是荒谬的,因为.cpp和.h的分离仅仅是.cpp可以单独编译和单独链接的地方,因为我们不能单独编译它们,因为模板是抽象的,因此我们总是被迫将抽象始终与具体实例化一起,具体实例化总是必须知道

意思是typename T在编译步骤中被替换而不是链接步骤所以如果我尝试编译模板而不将T替换为具体的值类型,那么它将无法工作,因为这是模板的定义,它是一个编译时进程,并且顺便说一句元编程就是使用这个定义。


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,尽管编译器会在此告诉您关于哪个运营商需要被覆盖的案例。


-2
投票

实际上,您可以在.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];
    }

229
投票

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

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

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

template class vector<int>;

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

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


216
投票

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

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

  • 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的.cpp。 foo.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)。


74
投票

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

有人可以说,编译器可以变得更聪明,可以“展望”模板的所有用途,但我确信创建递归或其他复杂场景并不困难。 AFAIK,编译器不会这样做。正如Anton所指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(但是?)。


58
投票

实际上,在C ++ 11之前,标准定义了export关键字,该关键字可以在头文件中声明模板并在其他地方实现它们。

没有一个流行的编译器实现了这个关键字。我所知道的唯一一个是Edison Design Group编写的前端,它由Comeau C ++编译器使用。所有其他人都要求您在头文件中编写模板,因为编译器需要模板定义才能进行正确的实例化(正如其他人已经指出的那样)。

因此,ISO C ++标准委员会决定使用C ++ 11删除模板的export功能。


34
投票

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

有一个导出关键字可以缓解这个问题,但它远不是可移植的。


27
投票

必须在头文件中使用模板,因为编译器需要实例化不同版本的代码,具体取决于为模板参数提供/推导的参数。请记住,模板不直接代表代码,而是代表该代码的多个版本的模板。在.cpp文件中编译非模板函数时,您正在编译具体的函数/类。对于可以使用不同类型实例化的模板,情况并非如此,即,在用具体类型替换模板参数时必须发出具体代码。

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

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

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


15
投票

这意味着定义模板类的方法实现的最便携方式是在模板类定义中定义它们。

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

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

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

© www.soinside.com 2019 - 2024. All rights reserved.