将模板化的C ++类拆分为.hpp / .cpp文件 - 是否可能?

时间:2009-11-12 17:40:11

标签: c++ class templates header linker

我在尝试编译C ++模板类时遇到错误,该类在.hpp.cpp文件之间分开:

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

这是我的代码:

stack.hpp

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

的main.cpp

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ld当然是正确的:符号不在stack.o

this question的回答无济于事,正如我所说的那样 This one可能会有所帮助,但我不想将每个方法都移到.hpp文件中 - 我不应该这样做,我应该吗?

唯一合理的解决方案是将.cpp文件中的所有内容移动到.hpp文件,并简单地包含所有内容,而不是作为独立的目标文件链接?这似乎非常丑陋!在这种情况下,我也可以恢复到以前的状态,并将stack.cpp重命名为stack.hpp并完成它。

16 个答案:

答案 0 :(得分:137)

无法在单独的cpp文件中编写模板类的实现并进行编译。所有这些方法,如果有人声称,是模拟单独的cpp文件的使用的解决方法,但实际上如果你打算编写模板类库并使用header和lib文件分发它来隐藏实现,那根本不可能。

要知道原因,让我们看一下编译过程。永远不会编译头文件。它们只是经过预处理。然后用预先编译的cpp文件对预处理的代码进行分组。现在,如果编译器必须为对象生成适当的内存布局,则需要知道模板类的数据类型。

实际上必须理解模板类根本不是类,而是类的模板,其声明和定义是在编译器在从参数获取数据类型的信息之后在编译时生成的。只要无法创建内存布局,就无法生成方法定义的指令。请记住,类方法的第一个参数是'this'运算符。所有类方法都转换为具有名称mangling的单个方法,并将第一个参数作为其操作的对象。 'this'参数实际上是告诉对象的大小,除非用户使用有效的类型参数实例化对象,否则编译器不能使用模板类。在这种情况下,如果将方法定义放在单独的cpp文件中并尝试编译它,则不会使用类信息生成目标文件本身。编译不会失败,它会生成目标文件,但不会为目标文件中的模板类生成任何代码。这就是链接器无法在目标文件中找到符号并且构建失败的原因。

现在隐藏重要实施细节的替代方案是什么?众所周知,将接口与实现分离的主要目的是以二进制形式隐藏实现细节。这是您必须分离数据结构和算法的地方。您的模板类必须仅表示数据结构而不是算法。这使您可以在单独的非模板化类库中隐藏更有价值的实现细节,其中的类可以在模板类上工作,或者只是使用它们来保存数据。模板类实际上包含较少的代码来分配,获取和设置数据。其余的工作将由算法类完成。

我希望这次讨论会有所帮助。

答案 1 :(得分:81)

是可能的,只要你知道你需要什么样的实例化。

在stack.cpp的末尾添加以下代码,它将起作用:

template class stack<int>;

将实例化堆栈的所有非模板方法,并且链接步骤将正常工作。

答案 2 :(得分:8)

你可以这样做

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

这已在Daniweb

中讨论过

同样在FAQ但使用C ++导出关键字。

答案 3 :(得分:6)

不,这是不可能的。不是没有export关键字,所有意图和目的并不存在。

您可以做的最好的事情是将您的函数实现放在“.tcc”或“.tpp”文件中,并将#tlude文件放在.hpp文件的末尾。然而,这仅仅是装饰性的;它仍然与在头文件中实现所有内容相同。这只是您使用模板支付的价格。

答案 4 :(得分:3)

我认为尝试将模板化代码分隔为标题和cpp有两个主要原因:

一个是纯粹的优雅。我们都喜欢编写易于阅读,管理和以后可重复使用的代码。

其他是减少编译时间。

我目前(一如既往)编码模拟软件与OpenCL结合使用,我们希望保留代码,以便根据硬件功能根据需要使用float(cl_float)或double(cl_double)类型运行。现在这是在代码开头使用#define REAL完成的,但这不是很优雅。更改所需的精度需要重新编译应用程序。由于没有真正的运行时类型,我们暂时不得不忍受这种情况。幸运的是,OpenCL内核是编译运行时,简单的sizeof(REAL)允许我们相应地改变内核代码运行时。

更大的问题是,即使应用程序是模块化的,在开发辅助类(例如那些预先计算模拟常量的类)时也必须模板化。这些类在类依赖关系树的顶部至少出现一次,因为最终模板类Simulation将具有这些工厂类之一的实例,这意味着几乎每次我对工厂类进行微小更改时,整个软件必须重建。这非常烦人,但我似乎无法找到更好的解决方案。

答案 5 :(得分:2)

有时可以将大部分实现隐藏在cpp文件中,如果可以将所有模板参数的常用功能提取到非模板类(可能是类型不安全)。然后header将包含对该类的重定向调用。当与“模板膨胀”问题作斗争时,使用类似的方法。

答案 6 :(得分:2)

问题是模板不会生成实际的类,它只是一个模板告诉编译器如何生成类。你需要生成一个具体的类。

简单而自然的方法是将方法放在头文件中。但还有另一种方式。

在.cpp文件中,如果您引用了所需的每个模板实例和方法,编译器将在那里生成它们以供整个项目使用。

new stack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}

答案 7 :(得分:2)

如果你知道你的堆栈将使用哪种类型,你可以在cpp文件中明确地实例化它们,并保留所有相关的代码。

也可以跨DLL(!)导出这些内容,但是使语法正确(__declspec(dllexport)和导出关键字的特定于MS的组合)非常棘手。

我们在数学/ geom lib中使用它,模板化了double / float,但是有很多代码。 (我当时在谷歌上搜索它,但今天没有那个代码。)

答案 8 :(得分:1)

您需要拥有hpp文件中的所有内容。问题是在编译器发现某些OTHER cpp文件需要它们之前,实际上并没有创建类 - 因此它必须拥有所有可用于编译模板化类的代码。

我倾向于做的一件事是尝试将模板拆分为通用的非模板化部分(可以在cpp / hpp之间拆分)和继承非模板化类的特定于类型的模板部分。 / p>

答案 9 :(得分:1)

仅在#include "stack.cpp结束时stack.hpp。我只推荐这种方法,如果实现相对较大,并且将.cpp文件重命名为另一个扩展名,以区别于常规代码。

答案 10 :(得分:1)

在创建库和标头组合并向用户隐藏实现时,可能要这样做。因此,建议的方法是使用显式实例化,因为您知道软件将交付什么,并且可以隐藏实现。

一些有用的信息在这里: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

对于您的相同示例: Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

输出:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

但是,我并不完全喜欢这种方法,因为通过将错误的数据类型传递给模板化类,这允许应用程序自行射击。例如,在main函数中,您可以传递其他可以隐式转换为int的类型,例如s.Push(1.2);。我觉得那很糟糕。

答案 11 :(得分:0)

由于模板是在需要时编译的,因此会强制限制多文件项目:模板类或函数的实现(定义)必须与其声明位于同一文件中。这意味着我们无法在单独的头文件中分离接口,并且我们必须在使用模板的任何文件中包含接口和实现。

答案 12 :(得分:0)

另一种可能性是做一些事情:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

我不喜欢这个建议,但它可能适合你。

答案 13 :(得分:0)

'export'关键字是将模板实现与模板声明分开的方法。这是在C ++标准中引入的,没有现有的实现。在适当的时候,只有几个编译器实际实现了它。深入了解Inform IT article on export

的信息

答案 14 :(得分:0)

1)记住分离.h和.cpp文件的主要原因是将类实现隐藏为单独编译的Obj代码,该代码可以链接到包含类的.h的用户代码。

2)非模板类具有在.h和.cpp文件中具体且具体定义的所有变量。因此,在编译/转换生成对象/机器代码之前,编译器将需要有关类中使用的所有数据类型的信息。 在类的用户实例化传递所需数据类型的对象之前,模板类没有关于特定数据类型的信息:

        TClass<int> myObj;

3)只有在此实例化之后,编译器才会生成模板类的特定版本以匹配传递的数据类型。

4)因此,.cpp不能在不知道用户特定数据类型的情况下单独编译。所以它必须作为源代码保留在“.h”中,直到用户指定所需的数据类型,然后,它可以生成特定的数据类型然后编译

答案 15 :(得分:-3)

我正在使用Visual Studio 2010,如果您想将文件拆分为.h和.cpp,请在.h文件的末尾包含您的cpp标头