Warning: file_get_contents(/data/phpspider/zhask/data//catemap/6/cplusplus/148.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C++ 为什么模板只能在头文件中实现?_C++_Templates_Undefined Reference_C++ Faq - Fatal编程技术网

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

C++ 为什么模板只能在头文件中实现?,c++,templates,undefined-reference,c++-faq,C++,Templates,Undefined Reference,C++ Faq,引自: 目前使用模板的唯一可移植方法是使用内联函数在头文件中实现它们 为什么会这样 澄清:头文件不是唯一的可移植解决方案。但是,它们是最方便的便携式解决方案。 < P>虽然标准C++没有这样的要求,但有些编译器要求在使用的每个翻译单元中都能使用所有的函数和类模板。实际上,对于这些编译器,模板函数体必须在头文件中可用。重复:这意味着这些编译器不允许在非头文件(如.cpp文件)中定义它们 有一个export关键字可以缓解这个问题,但它离可移植性还差得远。在将模板编译成目标代码之前,编译器需要实例化模

引自:

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

为什么会这样


澄清:头文件不是唯一的可移植解决方案。但是,它们是最方便的便携式解决方案。

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

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

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

有人可能会争辩说,编译器可以变得更智能,可以预见模板的所有用途,但我相信创建递归或其他复杂场景并不困难。顺便说一句,编译器不会这样做。正如Anton指出的,有些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它?

注意:不必将实现放在头文件中,请参阅本答案末尾的替代解决方案

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

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

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

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

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

福安

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}
这样,实现仍然与声明分离,但编译器可以访问

替代解决方案 另一种解决方案是将实现分开,并显式实例化您需要的所有模板实例:

福安

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

如果我的解释不够清楚,您可以看一下。

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

template < typename ... >
class MyClass
{

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

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

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


因此,ISO C++标准委员会决定用C++ 11删除模板的输出特性。

< P>这里有很多正确的答案,但我想补充这个:完整性:

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


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

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


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

这是完全正确的,因为编译器必须知道分配的类型。所以模板类、函数、枚举等等。。如果要将其公开或作为库静态或动态的一部分,则必须在头文件中也实现,因为头文件未被编译
e包含的c/cpp文件。如果编译器不知道类型为,则无法编译它。在.Net中,由于所有对象都派生自对象类,因此它可以。这不是.Net。

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

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

福安 声明类MyClass的接口 foo.cpp 定义类MyClass的实现 bar.cpp 使用MyClass 单独编译意味着我应该能够独立于bar.cpp编译foo.cpp。编译器完全独立地完成每个编译单元的所有分析、优化和代码生成的艰苦工作;我们不需要做整个程序的分析。只有链接器需要同时处理整个程序,而且链接器的工作要简单得多

编译foo.cpp时,bar.cpp甚至不需要存在,但我应该仍然能够将我已经拥有的foo.o与我刚刚生成的bar.o链接在一起,而不需要重新编译foo.cpp。foo.cpp甚至可以编译成动态库,在没有foo.cpp的情况下分发到其他地方,并与我编写foo.cpp多年后他们编写的代码相链接

实例化样式多态性意味着模板MyClass实际上不是一个泛型类,它不能编译成可用于任何t值的代码。这会增加开销,例如装箱,需要将函数指针传递给分配器和构造函数,C++模板的意图是避免编写几乎相同的类MyCaseLo.It、类MyCyclass、FLASH等,但仍然能够以编译代码结束,这主要是因为我们分别编写了每个版本。所以一个模板实际上就是一个模板;类模板不是类,它是为我们遇到的每个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 声明并实现类private,并使用MyClass 这是不可能的,除非我们

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

模板通常用于标题中,因为编译器需要实例化不同版本的代码,具体取决于为模板参数给定/推导的参数,而且作为程序员,让编译器多次重新编译同一代码并在以后消除重复比较容易。 请记住,模板并不直接表示代码,而是表示该代码的多个版本的模板。 在.cpp文件中编译非模板函数时,您正在编译具体的函数/类。 模板不是这种情况,它可以用不同的类型实例化,也就是说,用具体类型替换模板参数时必须发出具体代码

有一个带有export关键字的功能,用于单独编译。 导出功能在C++11中已被弃用,而且据了解,只有一个编译器实现了它。 你不应该利用出口。 单独编译是不可能的,在C++或C++ 11中,但可能在C++ 17,如果概念MAK 如果把它放进去,我们可以有一些单独编译的方法

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

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


编辑:到2020年8月,模块已经成为C++的现实:

尽管上面有很多很好的解释,但我缺少一种将模板分为页眉和正文的实用方法。 我主要关心的是在更改模板定义时避免重新编译所有模板用户。 在模板主体中包含所有模板实例化对我来说不是一个可行的解决方案,因为模板作者可能不知道它的用法,模板用户可能无权修改它。 我采用了以下方法,这也适用于较旧的编译器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
#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

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

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

但是,由于模板需要在编译步骤中进行处理,以便为您定义的每个模板实例化生成代码,因此,简单地将模板与其头文件分开编译是不可行的,因为它们总是并行不悖,因为每个模板实例化实际上都是一个全新的类。在常规类中,可以将.h和.cpp分开,因为.h是该类的蓝图,.cpp是原始实现,因此可以定期编译和链接任何实现文件,但是,使用templates.h是类的外观蓝图,而不是对象的外观蓝图,这意味着template.cpp文件不是类的原始常规实现,它只是类的蓝图,因此.h模板文件的任何实现都无法编译,因为您需要具体的编译内容,从这个意义上讲,模板是抽象的

因此,模板从不单独编译,只在其他源文件中有具体实例的地方编译。但是,具体的实例化需要知道模板文件的实现,因为在.h文件中使用具体类型简单地修改typename T不会起作用,因为有.cpp链接,我以后找不到它,因为记住模板是抽象的,无法编译,所以我必须马上给出实现,这样我就知道要编译和链接什么,现在我有了实现,它被链接到了封闭的源文件中。基本上,当我实例化一个模板时,我需要创建一个全新的类,如果我不知道该类在使用我提供的类型时应该是什么样子,我就不能这样做,除非我通知编译器模板实现,因此,现在编译器可以用我的类型替换T,并创建一个准备好编译和链接的具体类

总之,模板是类外观的蓝图,类是对象外观的蓝图。 因为编译器只编译具体类型,换句话说,至少在C++中的模板是纯语言抽象,所以我不能编译与它们的具体实例化分开的模板。可以说,我们必须对模板进行反抽象,我们通过给它们一个具体的类型来处理,这样我们的模板抽象就可以转换成一个常规的类文件,反过来,它就可以正常编译。分离template.h文件和template.cpp文件是没有意义的。这是荒谬的,因为.cpp和.h的分离只是.cpp可以单独编译并与模板单独链接,因为我们不能单独编译它们,因为模板是一种抽象,因此,我们总是被迫将抽象总是与具体的实例化放在一起,其中具体的实例化总是h 以了解正在使用的类型

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

从技术上讲,可以创建某种功能来保存template.cpp文件,并在其他源中找到类型时切换类型。我认为该标准确实有一个关键字导出,允许您将模板放在单独的cpp文件中,但实际上并不是很多编译器都实现了这一点


只是一个旁注,在对模板类进行专门化时,您可以将头与实现分开,因为根据定义专门化意味着我专门化的是可以单独编译和链接的具体类型。

如果问题是编译.h(作为使用它的所有.cpp模块的一部分)所产生的额外编译时间和二进制大小膨胀,在许多情况下,您可以做的是使模板类从接口的非类型依赖部分的非模板化基类派生,并且该基类可以在.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具有转发声明。tpp已实施,并包括内部_foo.h;而foo.h只有一行,包括foo.tpp

在编译时,foo.h的内容被复制到foo.tpp,然后整个文件被复制到foo.h,然后进行编译。这样,就没有限制,并且命名是一致的,以交换一个额外的文件


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

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

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;

    ...
}

实际上,您可以在.template文件而不是.cpp文件中定义模板类。无论谁说您只能在头文件中定义它都是错误的。这是一个工作方式,一直到C++ 98。 不要忘记让编译器将你的.file文件当作C++文件来保持智能。p> 下面是一个动态数组类的示例

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

在头文件中编写声明和定义是一个好主意的另一个原因是为了可读性。假设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类实现小于运算符我建议查看这个gcc页面,它讨论了模板实例化的cfront和borland模型之间的权衡

borland模型符合作者的建议,提供了完整的模板定义,并多次编译

它包含关于使用手动和自动模板实例化的明确建议。例如,-repo选项可用于收集需要实例化的模板。或者另一个选项是使用-fno隐式模板禁用自动模板实例化,以强制手动模板实例化

<>根据我的经验,我依赖C++模板库和Boost模板,使用模板库实例化每个编译单元。对于我的大型模板类,我对我需要的类型进行了一次手动模板实例化

这是我的方法,因为我提供的是一个工作程序,而不是用于其他程序的模板库。这本书的作者Josuttis在模板库方面做了很多工作

如果我真的担心速度,我想我会探索使用预编译头


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

为什么我不能用关键字inline在.cpp文件中实现它们?你可以,而且你甚至不必把它们放在inline中。但您可以在该cpp文件中使用它们,而不在其他任何地方使用。这几乎是最准确的答案,只是这意味着编译器不允许在非头文件(如.cpp文件)中定义它们。导出是标准的,但是它很难实现,所以大多数编译器团队还没有完成。导出并没有消除源代码公开的需要,也没有减少编译依赖性,同时它需要编译器构建者付出巨大的努力。因此,赫伯·萨特(Herb Sutter)自己要求编译器构建者“忘记”导出。由于所需的投资时间最好花在其他地方……所以我认为出口并不现实

“还没有”。如果你感兴趣的话,这篇论文叫《为什么我们负担不起出口》,它列在他的博客上,但没有pdf,谷歌应该快速打开它,好的,谢谢你的好例子和解释。但我的问题是:为什么编译器不能找出模板的调用位置,并在编译定义文件之前先编译这些文件?我可以想象它可以在一个简单的情况下完成。。。答案是相互依赖会很快打乱顺序吗?实际上,显式实例化需要在一个.cpp文件中,该文件可以访问所有Foo成员函数的定义,而不是在头文件中。编译器需要访问方法的实现,在本例中,使用模板参数int实例化它们。如果这些实现不在标头中,它们将无法访问,但为什么.cpp文件中的实现不可供编译器访问?编译器也可以访问.cpp信息,否则它将如何将它们转换为.obj文件?编辑:这个问题的答案在这个答案中提供的链接中……我认为这并不能很清楚地解释这个问题,关键的事情显然与编译单元有关,这在本文中没有提到post@Gabson:结构和类是等效的,但类的默认访问修饰符是私有的,而它对于结构是公共的。通过观察,你还可以了解到其他一些细微的差异。我在这个答案的开头添加了一句话,以澄清这个问题是基于一个错误的前提。如果有人问为什么X是真的?当事实上X不是真的时,我们应该很快拒绝这个假设。头文件没有被编译——这是一种非常奇怪的描述方式。头文件可以是翻译单元的一部分,就像c/cpp文件一样。事实上,这几乎与事实相反,头文件经常被编译多次,而源文件通常被编译一次。答案很好,但没有真正干净的解决方案。列出模板的所有可能类型似乎与模板的预期不符。在许多情况下,这可能很好,但通常会破坏模板的用途,即允许您将类与任何类型一起使用,而无需手动列出它们。vector不是一个很好的示例,因为容器天生以所有类型为目标类型。但创建的模板通常只针对特定类型集,例如数字类型:int8_t、int16_t、int32_t、uint8_t、uint16_t等。在这种情况下,使用模板仍然是有意义的,但为整个类型集显式实例化它们也是可能的,在我看来,推荐。在定义模板和所有成员函数后使用。谢谢我觉得我遗漏了什么…我将两种类型的显式实例化放在类的.cpp文件中,这两个实例化是从其他.cpp文件引用的,虽然将所有模板函数定义放入头文件可能是使用它们最方便的方式,但仍然不清楚内联在引用中做了什么。没有必要为此使用内联函数。内联与此完全无关。这本书已经过时了。模板不像一个可以编译成字节码的函数。这只是生成这样一个函数的一种模式。如果将模板单独放入*.cpp文件中,则无需编译任何内容。此外,显式的实例化实际上不是模板,而是从模板中创建函数的起点,它最终在*.Obf文件中结束。我是唯一一个觉得模板概念在C++中被削弱的人吗?……几年后,我终于明白了出口会给我们带来什么,还有什么不。。。现在我完全同意EDG的观点:@DevSolar:这篇论文是政治性的,重复性的,而且写得很糟糕。这不是通常的标准散文。太长太无聊了,在几十页的篇幅上说了三遍同样的话。但我现在被告知,出口不是出口。这是个好消息@v、 奥多:好的开发人员和好的技术作家是两种不同的技能。有些人两者都能做到,许多人不能-@v、 奥多:这篇论文不仅写得很糟糕,而且是虚假信息。此外,这也是对现实的一种歪曲:实际上,支持出口的极为有力的论据在某种程度上混杂在一起,让人觉得它们是反对出口的:“在存在导出的情况下发现标准中存在大量ODR相关漏洞。导出之前,ODR冲突不必由编译器进行诊断。现在这是必要的,因为您需要合并来自不同翻译单元的内部数据结构,并且
如果它们实际上代表了不同的事物,就不能将它们组合起来,所以你需要进行检查。”@DevSolar我仍然没有看到报纸上有反对出口的案例。我看到了一个导出的例子;类模板不是类,它是为每个T创建新类的方法encounter@Birger您应该能够从任何可以访问完整模板实现的文件中执行此操作,因为它位于同一文件中,或者通过头文件includes执行。@ajeh它不是。问题是,为什么要在头文件中实现模板,所以我解释了C++语言所导致的技术选择。在我写我的答案之前,其他人已经提供了不是完整解决方案的变通方法,因为不可能有完整的解决方案。我觉得这些答案会被一个关于为什么这个问题的更全面的讨论所补充。。。如果您没有使用模板来高效地编写所需的代码,那么无论如何,您只能提供该类的几个版本。所以你有3个选择。1.不要使用模板。像所有其他类/函数一样,没有人关心其他人不能更改类型2。使用模板,并记录它们可以使用的类型。3.给他们整个实现源奖金4。为他们提供完整的源代码,以防他们想从你的另一个类中创建模板@是的,从这个意义上讲,.tpp文件只是一种头文件的命名约定。头文件不是C++编译器所特有的,它只是我们称之为文件的文件,我们希望通过使用包含来将它包含到其他编译单元中。如果将模板实现与描述.cpp文件接口的文件放在一个单独的文件中,并为这些模板实现文件指定一个特定的扩展名,如.tpp,这有助于您使用代码,那么就去做吧!编译器不知道也不关心这个差异,但它可以帮助人类。这个响应应该修改得更多。我独立地发现了你的相同方法,并专门寻找其他人已经使用过它,因为我很好奇它是否是一种官方模式,是否有名字。我的方法是在需要实现模板类X的任何地方实现类XBase,将依赖类型的部分放在X中,其余部分放在XBase.s/internal_foo/foo/g中,并在foo.h末尾包含foo.tpp。少了一个文件。除了MyInstancedTemplate.h文件和添加了MyInstancedTemplate类型之外,我喜欢这种方法。如果你不用它,它会干净一点。请验证我对另一个问题的回答:这需要两个世界中最好的一个。我希望这个答案被评得更高!另请参阅上面的链接,以获得对同一想法更清晰的实现。大多数人将头文件定义为任何将定义传播到源文件的内容。因此,您可能已经决定使用文件扩展名.template,但您已经编写了一个头文件。对于real man???如果这是真的,那么您的答案应该被检查为正确的。如果您可以在.cpp中定义非模板成员方法,为什么会有人需要所有这些黑巫毒的东西呢?这是行不通的。至少在MSVC 2019上是如此,正在为模板类的成员函数获取未解析的外部符号。我没有要测试的MSVC 2019。这是C++标准允许的。现在,MSVC因不总是遵守规则而臭名昭著。如果您还没有,请尝试项目设置->C/C++->语言->一致性模式->是许可-.这个确切的示例可以工作,但您不能从myQueue.cpp之外的任何其他翻译单元调用isEmpty…因此,这可能是将大量函数移动到.cpp文件并将其声明为私有的一个好策略,而公共函数保留在头文件中并调用它们。到目前为止,我知道在通用的模块编译器中有一些实现。
template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    
// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}
#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 <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];
    }
template <class T>
T min(T const& one, T const& theOther);
#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}