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

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

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

这是为什么?

(说明:标头文件不是 only 便携式解决方案,而是最方便的便携式解决方案。)

标签:c++,templates,c++-faq

回答1:

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

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

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 ++超级常见问题解答

回答2:

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

如果在实现cpp文件的底部,对模板将使用的所有类型进行显式实例化,则链接程序将能够照常查找它们。

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

template class vector<int>;

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

上面的示例是毫无用处的,因为矢量完全在标头中定义,除非当公共包含文件(预编译标头?)使用extern模板类vector 来防止实例化时所有使用vector的 other (1000?)文件中。

回答3:

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

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

  • foo.h
    • 声明classMyClass
    • 的接口
  • foo.cpp
    • 定义了classMyClass
    • 的实现
  • bar.cpp
    • 使用MyClass

单独编译意味着我应该能够独立于 bar.cpp 编译 foo.cpp 。编译器完全独立地在每个编译单元上进行分析,优化和代码生成的所有艰苦工作。我们不需要进行整个程序分析。只是链接程序需要立即处理整个程序,而链接程序的工作实际上要容易得多。

编译 foo.cpp 时甚至不需要

bar.cpp ,但是我仍然应该能够链接 foo.o < / strong>我已经与刚刚制作的 bar.o 在一起,而无需重新编译 foo.cpp foo.cpp 甚至可以编译成动态库,无需 foo.cpp 即可分发到其他位置,并与他们在我编写 foo.cpp多年后编写的代码链接

"实例化样式多态性"表示模板MyClass 并不是真正的泛型类,可以将其编译为可用于任何T。这将增加诸如装箱的开销,需要将函数指针传递给分配器和构造函数等。C++模板的目的是避免编写几乎相同的classMyClass_intclassMyClass_float等,但最终仍然可以得到编译后的代码,就像我们必须分别编写每个版本一样。因此,模板实际上是 模板;类模板不是类,而是为我们遇到的每个 T 创建一个新类的秘诀。模板不能编译成代码,只能编译实例化的结果。

因此,在编译 foo.cpp 时,编译器看不到 bar.cpp 来知道需要MyClass 。它可以看到模板MyClass ,但不能为此发出代码(它是模板,而不是类)。并且在编译 bar.cpp 时,编译器可以看到它需要创建一个MyClass ,但看不到模板MyClass (仅其接口位于 foo.h 中),因此无法创建它。

如果 foo.cpp 本身使用MyClass ,则将在编译 foo.cpp 时生成该代码,因此当 bar.o 已链接到 foo.o ,它们可以连接并起作用。我们可以利用这一事实,通过编写单个模板,在.cpp文件中实现一组有限的模板实例化。但是 bar.cpp 无法将模板用作模板,并根据需要的类型对其进行实例化;它只能使用 foo.cpp 的作者提供的模板类的现有版本。

您可能会认为,编译模板时,编译器应"生成所有版本",并且在链接过程中会滤除从未使用过的版本。除了庞大的开销和极端的困难之外,这种方法还会面临困难,因为指针和数组之类的"类型修饰符"功能甚至允许内置类型也可以产生无限数量的类型,当我现在扩展程序时会发生什么通过添加:

  • baz.cpp
    • 声明并实现BazPrivate类,并使用MyClass

除非我们要么

,否则不可能有任何可行的方法
  1. 每当我们更改程序中的任何其他文件时,都必须重新编译 foo.cpp ,以防它添加了MyClass 的新实例.
  2. 要求 baz.cpp 包含(可能通过标头包含)MyClass 的完整模板,以便编译器可以生成MyClass 在编译 baz.cpp 的过程中。

没人喜欢(1),因为整个程序分析编译系统需要 forever 来进行编译,并且因为没有源代码就不可能分发已编译的库。所以我们改为(2)。

回答4:

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

有人可以说可以使编译器变得更聪明,以"预见"模板的所有用途,但是我敢肯定,创建递归或其他复杂的场景并不困难。 AFAIK,编译器不会提前这样做。正如Anton所指出的那样,一些编译器支持模板实例的显式导出声明,但并非所有编译器都支持(实例?)。

回答5:

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

>

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

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

回答6:

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

有一个 export 关键字可以缓解此问题,但距离可移植性还很遥远。

回答7:

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

有一个带有export关键字的功能,该功能旨在用于单独的编译。 export功能已在C++11中弃用,并且AFAIK仅由一个编译器实现。您不应该使用export。在C++C++11中不可能进行单独的编译,但是在C++17中可能进行编译,如果有概念的话,我们可以单独编译的某种方式。

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

模板的单独编译问题我想这也是由于迁移到模块而引起的,目前正在解决。

回答8:

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

template < typename ... >
class MyClass
{

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

回答9:

即使上面有很多很好的解释,我也缺少将模板分为标头和正文的实用方法。
我主要担心的是在更改模板定义时避免重新编译所有模板用户。
>在模板主体中包含所有模板实例化对我来说不是一个可行的解决方案,因为模板作者可能无法完全了解其用法,并且模板用户可能无权对其进行修改。
我采用了以下方法,该功能也适用于较早的编译器(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

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

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

回答10:

如果担心的是通过将.h编译为使用所有.cpp模块的一部分而产生的额外的编译时间和二进制大小膨胀,那么在许多情况下,您可以做的是使模板类从非模板化的子类中继承接口的与类型无关的基本类,并且该基本类可以在.cpp文件中实现。

回答11:

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

回答12:

只需在此处添加一些值得注意的内容即可。当它们不是函数模板时,可以在实现文件中很好地定义模板类的方法。


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:

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

但是,由于模板需要在编译步骤中进行处理才能为您定义的每个模板实例生成代码,因此仅将模板与其头文件分开编译是行不通的,因为它们总是并存的从字面上看,每个模板实例化都是一个全新的类。在常规类中,您可以将.h和.cpp分开,因为.h是该类的蓝图,而.cpp是原始实现,因此任何实现文件都可以定期编译和链接,但是使用模板.h是如何实现的蓝图类应该看起来不像对象看起来的样子,意味着模板.cpp文件不是类的原始常规实现,它只是类的蓝图,因此无法编译任何.h模板文件的实现,因为您需要具体的东西来进行编译,从这个意义上说,模板是抽象的。

因此,模板永远不会单独编译,仅在您在某些其他源文件中有具体实例化的位置才编译。但是,具体的实例化需要知道模板文件的实现,因为仅使用.h文件中的具体类型修改typenameT并不能完成任务,因为其中存在.cpp链接,我以后找不到它,因为记住模板是抽象的并且不能编译,所以我被迫现在给出实现,所以我知道要编译和链接什么,并且现在有了实现它被链接到封闭的源文件中。基本上,实例化模板的那一刻,我需要创建一个全新的类,并且如果不知道使用所提供的类型时该类的外观,则无法这样做,除非我通知编译器模板实现,因此现在编译器可以用我的类型替换T并创建一个准备好进行编译和链接的具体类。

总而言之,模板是类外观的蓝图,类是对象外观的蓝图。我无法将模板与其具体实例分开进行编译,因为编译器仅编译具体类型,换句话说,至少在C ++中,模板是纯语言抽象。可以这么说,我们必须取消模板的抽象,我们通过为它们提供一种具体的类型来进行处理,以便模板抽象可以转换为常规类文件,然后可以正常地对其进行编译。将模板.h文件和模板.cpp文件分开是没有意义的。这是没有意义的,因为仅.cpp和.h的分隔仅是.cpp可以单独编译和单独链接的地方,因为我们不能单独编译它们,因为模板是抽象的,因此我们总是被迫将抽象始终与具体实例结合在一起,具体实例必须始终了解所使用的类型。

意思是typenameT在编译步骤而不是链接步骤中被替换,因此,如果我尝试编译一个模板而没有将T替换为完全是具体值的类型对于编译器而言毫无意义,因此无法创建目标代码,因为它不知道T是什么。

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

请注意,在对模板类进行专业化处理时,可以将标头与实现分开,因为按定义进行的专业化处理意味着我专门针对可以单独编译和链接的具体类型。

回答14:

单独实施的方法如下。

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

回答15:

在头文件中同时写入声明和定义是一个好主意的另一个原因是为了提高可读性。假设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:

我建议查看此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

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

回答17:

您实际上可以在.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];
    }
回到顶部