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

2256

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

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

为什么是这样呢?

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


29
将所有模板函数定义放在头文件中确实是使用它们最方便的方式,但是引用中的“inline”到底是什么意思还不清楚。没有必要为此使用内联函数,“inline”与此毫无关系。 - AnT stands with Russia
17
书已经过时。 - gerardw
17
一个模板不同于一个可以编译成字节码的函数,它只是一种生成这种函数的模式。如果你把一个模板单独放在一个 *.cpp 文件中,就没有什么东西可以编译。此外,显式实例化实际上并不是一个模板,而是将模板转化为函数的起点,最终生成一个 *.obj 文件。 - dgrat
44
因为这个原因,我是否是唯一一个感觉C++中的模板概念受到了损害的人? - DragonGamer
4
@AnT 或许他们所说的“inline”并不是关键字,而是指“在类内部声明时实现的方法”。 - Vorac
显示剩余5条评论
19个回答

11

在这里补充一些值得注意的内容。如果它们不是函数模板,那么可以在实现文件中定义模板类的方法。


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;

    ...
}

3
真的吗???如果是真的,那么你的答案应该被标记为正确的。如果可以在 .cpp 中定义非模板成员方法,那么为什么任何人都需要所有那些 hacky voodoo 的东西呢? - Michael IV
那不起作用。至少在MSVC 2019上,对于模板类的成员函数,会得到未解决的外部符号。 - Michael IV
我没有MSVC 2019来测试。这是C++标准允许的。现在,MSVC因不总是遵守规则而臭名昭著。如果您还没有尝试过,请尝试项目设置-> C / C ++->语言->符合模式->是(宽容-)。 - KeyC0de
3
这个具体的例子可以工作,但是除了myQueue.cpp之外,您不能从任何其他翻译单元调用isEmpty函数。 - M.M
这可以是将臃肿的函数移动到.cpp文件并将它们声明为私有的好策略,同时公共函数保留在头文件中并调用它们。 - Abhinav Gauniyal

10

如果担忧在将.h文件作为所有使用它的.cpp模块的一部分进行编译时所产生的额外编译时间和二进制文件大小的膨胀,很多情况下您可以使模板类从一个非模板化基类继承其接口的非类型相关部分,并且该基类可以在.cpp文件中实现。


2
这个回复应该被更加重视。我 "独立地" 发现了与您相同的方法,并且特别寻找其他人是否已经使用过它,因为我很好奇它是否是一个 正式的设计模式 ,以及是否有名称。我的方法是在需要实现 template class X 的任何地方实现一个 class XBase,将依赖于类型的部分放在 X 中,所有其余部分放在 XBase 中。 - Fabio A.

8
一种实现分离的方法如下所示。
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.h 包含了前向声明。 foo.tpp 包含了实现并引用了 inner_foo.h, 而 foo.h 只需一行代码,将引用 foo.tpp

在编译时,foo.h 的内容会被复制到 foo.tpp,然后整个文件会被复制到 foo.h,之后进行编译。这种方式没有限制,并且命名一致,代价是多出一个文件。

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


5
将s/inner_foo/foo/g应用于foo文件,并在foo.h的末尾包含foo.tpp。少了一个文件。 - user246672
user246672有一点错误——只需在需要它们的".cpp"文件中包含“.tpp”文件(我使用“.ft”)。 - Spencer

7

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


7
“头文件不被编译”这样描述的方式非常奇怪。 头文件可以像“c/cpp”文件一样成为翻译单元的一部分。 - Flexo
4
实际上,这与事实几乎相反。头文件通常会被多次编译,而源代码文件通常只会被编译一次。 - xaxxon

3
我建议查看这个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,许多编译器正在支持它。但是,我认为使用模板头文件会使预编译头文件变得困难。"

2
(从关闭的重复中复制到这里)
我更喜欢将所有函数放在.cpp文件中,无论是模板函数还是普通函数。而且有一种方法可以通过一些基本的#ifndef魔术来实现。以下是你可以做的:

main.cpp

#include "myclass.hpp"

int main()
{
  // ...
}

myclass.hpp

#ifndef MYCLASS
#define MYCLASS

template<class T>
class MyClass
{
  T val;
public:
  MyClass(T val_);
}

#define MYCLASS_FUNCTIONS
#include "myclass.cpp"

#endif

myclass.cpp

#ifndef MYCLASS_FUNCTIONS
#include "myclass.hpp"

// regular functions:
// ...

#else
 
// template functions:
template<class T>
MyClass<T>::MyClass(T val_)
    :val(val_)
{}

// ...
#endif

这是预编译器的视角。我们有两个`.cpp`文件。
当我们编译main.cpp时:
  1. 包含myclass.hpp
  2. 检查MYCLASS未定义,确实未定义
  3. 定义MYCLASS
  4. 将生成的类的定义(来自模板类)提供给编译器
  5. 包含myclass.cpp
  6. 定义MYCLASS_FUNCTIONS
  7. 检查MYCLASS_FUNCTIONS是否已定义,确实已定义
  8. 将生成的函数的定义(来自模板函数)提供给编译器
  9. 当我们编译myclass.cpp时
  10. 检查MYCLASS_FUNCTIONS是否已定义,确实未定义
  11. 包含myclass.hpp
  12. 检查MYCLASS未定义,确实未定义
  13. 定义MYCLASS
  14. 将类的定义提供给编译器
  15. 包含myclass.cpp
  16. 再次包含myclass.hpp
  17. 这次MYCLASS已定义,因此在内部不执行任何操作,返回到myclass.cpp
  18. 检查MYCLASS_FUNCTIONS是否已定义,确实已定义
  19. 将生成的函数的定义(来自模板函数)提供给编译器
  20. 退出两次包含
  21. 将所有常规函数传递给编译器

0

受到Moshe在https://dev59.com/O3RB5IYBdhLWcg3w1Kr0#38448106的回答的启发,我也做出了一点小贡献,提供一个扩展示例。假设有一个总体的OperationSuccess,其中包含一个具有通用类型的ResponseSuccess。

ResponseSuccess.h

template <class T>
class ResponseSuccess {
public:
    ResponseSuccess(const ResponseStatus responseStatus, const T& data) :
        m_responseStatus(responseStatus),
        m_data(data) {}

    ~ResponseSuccess() = default;

    // Basis requirement, have Copy/Move constructor/delete assignment operator

    ResponseStatus getResponseStatus() const {
        return m_responseStatus;
    }

    T getData() const {
        return m_data;
    };

private:
    ResponseStatus m_responseStatus;

    T m_data;
};

OperationSuccess.h

template <class T>
class OperationResponse {
public:
    explicit OperationResponse(ResponseSuccess<T> responseSuccess) :
        m_responseSuccess(std::move(responseSuccess)) {}

    ~OperationResponse() = default;
    // Basis requirement, have Copy/Move constructor/delete assignment operator

    ResponseSuccess<T> getResponseSuccess() const {
        return m_responseSuccess;
    }

private:
    ResponseSuccess<T> m_responseSuccess;
    // have a failure, in case required
};

使用方法:

MyObject myObj(<ctor_args>);
    ResponseSuccess<MyObject> responseSuccess(ResponseStatus::SUCCESS, myObj);
    OperationResponse<MyObject> successOperationResponse(responseSuccess);
..
// Fetches the response -> successOperationResponse.getResponseSuccess();

-5
另一个将声明和定义都写在头文件中的好处是为了可读性。假设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,尽管在这种情况下编译器会告诉您需要覆盖哪个运算符。


-12

我必须编写一个模板类,这个例子对我很有帮助。

这是一个动态数组类的示例。

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

12
大多数人会把头文件定义为将定义传递给源文件的任何内容。因此,您可能已经决定使用扩展名“.template”,但实际上编写了一个头文件。 - Tommy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接