关于C++:为什么模板只能在头文件中实现?

Why can templates only be implemented in the header file?

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

The only portable way of using templates at the moment is to implement them in header files by using inline functions.

为什么会这样?

(说明:头文件不是唯一的可移植解决方案。但它们是最方便的便携式解决方案。)


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

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

1
2
3
4
5
6
7
8
9
template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f;

在读取此行时,编译器将创建一个新类(我们称之为FooInt),相当于以下内容:

1
2
3
4
5
struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

因此,编译器需要访问这些方法的实现,并使用模板参数(在本例中为int)来实例化它们。如果这些实现不在头中,它们将不可访问,因此编译器将无法实例化模板。

一个常见的解决方案是在头文件中编写模板声明,然后在实现文件(例如.tpp)中实现类,并在头文件的末尾包含这个实现文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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
}

这样,实现仍然与声明分离,但编译器可以访问。

另一个解决方案是保持实现分离,并显式地实例化您需要的所有模板实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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++超级常见问题。


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

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

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

1
template class vector<int>;

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

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


这是因为需要单独编译,而且模板是实例化风格的多态性。

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

  • 福奥
    • 声明class MyClass的接口
  • 英尺·CPP
    • 定义class MyClass的实现
  • 巴普
    • 使用MyClass

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

当我编译foo.cpp时,bar.cpp甚至不需要存在,但是我仍然可以将我已经拥有的foo.o与bar.o链接起来,而不需要重新编译foo.cpp。甚至可以将foo.cpp编译到动态库中,在没有foo.cpp的情况下分发到其他地方,并与我编写foo.cpp几年后编写的代码链接。

"实例化样式多态性"意味着模板MyClass实际上不是一个可编译为可用于T任何值的代码的通用类。这将增加诸如拳击之类的开销,需要传递给分配器和构造函数的函数指针等。C++模板的意图是避免编写几乎相同的EDCOX1×5,EDCOX1,6,等等,但仍然能够以编译代码结束,这主要是因为我们分别编写了每个版本。因此,模板实际上是一个模板;类模板不是类,而是为我们遇到的每个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作者认为提供的模板类的现有版本。

您可能认为,在编译模板时,编译器应该"生成所有版本",在链接期间过滤掉从未使用过的版本。除了巨大的开销和极端的困难之外,这种方法还将面临这样的困难,因为"类型修饰符"特性(如指针和数组)甚至允许内置类型产生无限多的类型,当我现在通过添加以下内容来扩展程序时会发生什么:

  • 巴斯普
    • 申报实施class BazPrivate,使用MyClass

这是不可能的,除非我们

  • 每次我们更改程序中的任何其他文件时都必须重新编译foo.cpp,以防它添加了新的MyClass实例化。
  • 要求baz.cpp包含(可能通过header includes)MyClass的完整模板,以便编译器在编译baz.cpp时生成MyClass
  • 没有人喜欢(1),因为整个程序分析编译系统需要花很长时间来编译,而且如果没有源代码,就不可能分发已编译的库。所以我们有(2)代替。


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

    有人认为编译器可以更智能地"前瞻"模板的所有用途,但我敢肯定,创建递归或其他复杂的场景并不困难。阿法克,编纂者不会这样看。正如安东指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(还没有?).


    实际上,在C++ 11之前,标准定义了EDCOX1×7 }关键字,这使得可以在头文件中声明模板并在别处实现它们。

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

    因此,ISO C++标准委员会决定用C++ 11删除模板的EDCOX1 7特征。


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

    有一个export关键字可以缓解这个问题,但它离可移植性还差得远。


    必须在头文件中使用模板,因为编译器需要根据为模板参数给定/推导的参数实例化不同版本的代码。请记住,模板并不直接表示代码,而是该代码的多个版本的模板。当您在.cpp文件中编译一个非模板函数时,您正在编译一个具体的函数/类。模板不是这样的,模板可以用不同的类型进行实例化,也就是说,当用具体的类型替换模板参数时,必须发出具体的代码。

    有一个带有export关键字的特性,用于单独编译。export功能在C++11中已被弃用,而在afaik中,只有一个编译器实现了它。你不应该利用export。单独编译在C++C++11中是不可能的,但在C++17中可能是这样,如果概念能够实现,我们可以有某种单独编译的方法。

    要实现单独的编译,必须可以单独检查模板主体。似乎可以用概念来解决问题。看看最近发表在标准委员会会议。我认为这不是唯一的要求,因为您仍然需要在用户代码中为模板代码实例化代码。

    模板的单独编译问题,我想也是迁移到当前正在工作的模块时出现的问题。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template < typename ... >
    class MyClass
    {

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

    尽管上面有很多很好的解释,但是我缺少一种将模板分为头和体的实用方法。我主要关心的是在更改模板定义时避免对所有模板用户重新编译。在模板主体中拥有所有模板实例化对我来说不是一个可行的解决方案,因为模板作者可能不知道它的用法,模板用户可能无权修改它。我采用了以下方法,也适用于较旧的编译器(GCC 4.3.4,acc A.03.13)。

    对于每个模板的使用,在它自己的头文件(从UML模型生成)中都有一个typedef。它的主体包含实例化(最后在一个库中链接)。模板的每个用户都包含该头文件并使用typedef。

    示意图示例:

    MyTime.H:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #ifndef MyTemplate_h
    #define MyTemplate_h 1

    template <class T>
    class MyTemplate
    {
    public:
      MyTemplate(const T& rt);
      void dump();
      T t;
    };

    #endif

    MyTemplate.cpp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #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;
    }

    myInstantatedTemplate.h(我的实例化模板.h):

    1
    2
    3
    4
    5
    6
    7
    #ifndef MyInstantiatedTemplate_h
    #define MyInstantiatedTemplate_h 1
    #include"MyTemplate.h"

    typedef MyTemplate< int > MyInstantiatedTemplate;

    #endif

    myInstantatedTemplate.cpp:

    1
    2
    3
    #include"MyTemplate.cpp"

    template class MyTemplate< int >;

    MCP.CPP:

    1
    2
    3
    4
    5
    6
    7
    8
    #include"MyInstantiatedTemplate.h"

    int main()
    {
      MyInstantiatedTemplate m(100);
      m.dump();
      return 0;
    }

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


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


    这是完全正确的,因为编译器必须知道它用于分配的类型。所以模板类、函数、枚举等。如果要使头文件成为公共文件或库的一部分(静态或动态),则必须在头文件中也实现,因为头文件与C/CPP文件不同,它们是不编译的。如果编译器不知道类型是什么,就无法编译它。在.NET中,它可以,因为所有对象都是从对象类派生的。这不是.NET。


    单独实现的方法如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //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>

    内部foo具有转发声明。foo.tpp实现并包含内部foo.h;foo.h只有一行,包括foo.tpp。

    编译时,将foo.h的内容复制到foo.tpp,然后将整个文件复制到foo.h,然后进行编译。这样,就不存在任何限制,而且命名是一致的,只需要一个额外的文件。

    我这样做是因为代码的静态分析器在它看不到*.tpp中类的前向声明时会中断代码。在任何IDE中编写代码或使用completeme或其他代码时,这都很烦人。


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

    MyQueal.HPP:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template <class T>
    class QueueA {
        int size;
        ...
    public:
        template <class T> T dequeue() {
           // implementation here
        }

        bool isEmpty();

        ...
    }

    MyQueal.CPP:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // implementation of regular methods goes like this:
    template <class T> bool QueueA<T>::isEmpty() {
        return this->size == 0;
    }


    main()
    {
        QueueA<char> Q;

        ...
    }

    在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。在编译和链接过程中,.cpp文件被转换为纯对象或计算机代码,其中包含引用或未定义的符号,因为包含在main.cpp中的.h文件尚未实现。这些文件可以链接到另一个对象文件,该文件为模板定义了一个实现,因此您有一个完整的a.out可执行文件。但是,由于需要在编译步骤中处理模板,以便为您在主程序中执行的每个模板实例化生成代码,因此链接将没有帮助,因为将main.cpp编译为main.o,然后将template.cpp编译为template.o,然后链接将无法实现模板目的,因为我正在链接同一模板实现的模板实例化不同!模板应该做相反的事情,即有一个实现,但允许通过使用一个类进行许多可用的实例化。

    这意味着typename Tget在编译步骤中被替换,而不是链接步骤,所以如果我试图编译一个没有T被替换为具体值类型的模板,那么它将不起作用,因为这是模板的定义,它是一个编译时过程,而btw元编程就是使用这个定义。


    在头文件中编写声明和定义是一个好主意的另一个原因是为了可读性。假设在utility.h中有这样一个模板函数:

    1
    2
    template <class T>
    T min(T const& one, T const& theOther);

    在utility.cpp中:

    1
    2
    3
    4
    5
    6
    #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的东西。

    不要忘记让你的编译器把你的.file文件当作C++文件来保持智能。

    下面是一个动态数组类的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #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文件中,您按照通常的方式定义函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    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];
        }