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


96

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

$ 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

这个问题的答案无济于事,正如我所说的那样。
可能会有所帮助,但是我不想将每个方法都移动到.hpp文件中,我不必,应该吗?

.cpp文件中的所有内容移动到.hpp文件中并仅包含所有内容而不是将其作为独立目标文件链接的唯一合理解决方案吗?这似乎非常难看!在这种情况下,我还不如恢复到我以前的状态,并重新命名stack.cpp,以stack.hpp和与它做。


当您要真正隐藏代码(在二进制文件中)或保持代码干净时,有两种不错的解决方法。尽管在第一种情况下,还是需要降低通用性。此处说明:stackoverflow.com/questions/495021/…–
Sheric

Answers:


151

无法在单独的cpp文件中编写模板类的实现并进行编译。如果有人声称,这样做的所有方法都是模仿单独cpp文件使用的解决方法,但是实际上,如果您打算编写模板类库并将其与标头和lib文件一起分发以隐藏实现,则根本不可能。

要知道为什么,让我们看一下编译过程。头文件从不编译。它们仅经过预处理。然后将预处理的代码与实际编译的cpp文件合并。现在,如果编译器必须为对象生成适当的内存布局,则它需要知道模板类的数据类型。

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

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

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


2
“必须理解,模板类根本不是类”-不是吗?类模板是模板。有时使用“模板类”代替“模板的实例化”,并且它将是实际的类。
Xupicor

仅供参考,没有解决方法是不正确的!将数据结构与方法分开也是一个坏主意,因为它与封装相反。有一个很好的解决方法,您可以在某些情况下(我相信最多)在这里使用:stackoverflow.com/questions/495021/…–
Sheric

@Xupicor,你是对的。从技术上讲,“类模板”是您编写的,因此您可以实例化“模板类”及其对应的对象。但是,我相信,在通用术语中,两个术语可以互换使用不会有那么大的错误,用于定义“类模板”本身的语法以单词“模板”而不是“类”开头。
Sharjith N.

@Sheric,我没有说没有解决方法。实际上,在模板类的情况下,所有可用的方法仅是模拟接口和实现分离的变通方法。如果不实例化特定类型的模板类,则这些变通办法都不起作用。无论如何,这消除了使用类模板的通用性。将数据结构与算法分开不同于将数据结构与方法分开。数据结构类可以很好地具有构造函数,getter和setter之类的方法。
Sharjith N.

我刚刚发现要完成这项工作的最接近方法是使用一对.h / .hpp文件,并在.h文件末尾使用#include“ filename.hpp”定义模板类。(在分号的类定义的右括号下方)。这至少在结构上按文件将它们分开,这是允许的,因为最后,编译器会将您的.hpp代码复制/粘贴到您的#include“ filename.hpp”上。
Artorias2718

90

可能的,只要你知道实例,你将需要。

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

template class stack<int>;

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


7
实际上,大多数人为此使用单独的cpp文件-类似于stackinstantiations.cpp。
Nemanja Trifunovic

@NemanjaTrifunovic您能举个stackinstantiations.cpp的例子吗?
qwerty9967 2013年

3
其实还有其他解决方案:codeproject.com/Articles/48575/…–
sleepsort

@Benoît我收到一个错误错误:';'之前的预期unqualified-id 令牌模板堆栈<int>; 你知道为什么吗?谢谢!
camino

3
实际上,正确的语法是template class stack<int>;
Paul Baltescu 2014年

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 ++ export关键字。


5
include荷兰国际集团一个cpp文件通常是一个可怕的想法。即使您有正当的理由,也应该给文件(实际上只是美化的标头)加上一个hpp或不同的扩展名(例如tpp),以清楚地说明正在发生的事情,消除makefile针对实际 cpp文件的混乱等。
underscore_d

@underscore_d您能否解释为什么包含.cpp文件是一个糟糕的主意?
阿巴斯

1
@Abbas,因为扩展名cpp(或cc,或c或任何其他名称)指示文件是实现的一部分,结果转换单元(预处理器输出)可单独编译,并且文件的内容仅编译一次。它并不表示该文件是接口的可重用部分,可以任意包含在任何位置。#include荷兰国际集团的实际 cpp文件将迅速填满你的屏幕与多个定义的错误,这是正确的。在这种情况下,因为一个理由#include吧,cpp只是延长了错误的选择。
underscore_d

@underscore_d因此,仅将.cpp扩展名用于此类用途基本上是错误的。但是使用另一种说法.tpp是完全可以的,这将达到相同的目的,但使用不同的扩展名可以更容易/更快速地理解?
阿巴斯

1
@Abbas是的,cpp/ cc在/ etc必须避免,但它是一个好主意,比其他使用的东西hpp-比如tpptcc等等-这样你就可以重复使用的文件名的其余部分,并表示该tpp文件,但它就像一个头,在相应的中保留模板声明的离线实现hpp。因此,本文的开头是一个好的前提-将声明和定义分离到2个不同的文件中,这可能更容易grok / grep,或者有时由于循环依赖IME而需要-后来以建议第二个文件具有错误的扩展名结尾
underscore_d

6

不,不可能。并非没有export关键字,对于所有意图和目的而言,关键字实际上都不存在。

最好的办法是将函数实现放在“ .tcc”或“ .tpp”文件中,并在.hpp文件末尾#include .tcc文件。但是,这仅仅是装饰性的;它仍然与在头文件中实施所有操作相同。这只是您使用模板所要付出的代价。


3
您的答案不正确。只要知道要使用的模板参数,就可以从cpp文件中的模板类生成代码。请参阅我的答案以获取更多信息。
贝诺瓦

2
的确如此,但这受到了严重的限制,即每次引入使用模板的新类型时都需要更新.cpp文件并重新编译,这可能不是OP所考虑的。
Charles Salvia

3

我相信将模板代码分为标头和cpp的主要原因有两个:

一个是纯粹的优雅。我们所有人都喜欢编写浪费时间阅读,管理并在以后可重用的代码。

其他是减少编译时间。

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

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


2

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


4
如果执行此操作,则需要将#ifndef STACK_CPP(和朋友)添加到stack.cpp文件中。
Stephen Newell,

击败我这个建议。由于风格原因,我也不喜欢这种方法。
卢克

2
是的,在这种情况下,绝对不应给第二个文件扩展名cppcc或其他),因为这与其实际作用形成了鲜明的对比。相反,应该给它一个不同的扩展名,以指示它是(A)标头,并且(B)标头包含在另一个标头的底部。我用tpp这个,它轻而易举地也可以代表tEM p晚IM plementation(超出行定义)。我漫步更多关于此位置:stackoverflow.com/questions/1724036/...
underscore_d

2

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


+
1-

2

如果您知道将使用哪种类型的堆栈,则可以在cpp文件中显式实例化它们,并将所有相关的代码保存在那里。

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

我们已经在以double / float为模板的math / geom库中使用了它,但是有很多代码。(我当时在Google上搜索,但是今天没有该代码。)


2

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

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

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

新的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
}

8
您不需要dummey函数:使用'template stack <int>;' 这将模板实例化为当前的编译单元。如果您定义模板,但只希望在共享库中有几个特定的​​实现,则非常有用。
马丁·约克

@Martin:包括所有成员函数吗?这太妙了。您应该将此建议添加到“隐藏的C ++功能”线程中。
Mark Ransom,

@LokiAstari我发现了关于此的文章,以防有人想了解更多:cplusplus.com/forum/articles/14272
Andrew Larsson

1

您需要将所有内容都放在hpp文件中。问题在于,直到编译器发现某个其他cpp文件需要它们之后才真正创建这些类-因此它当时必须具有所有可用代码来编译模板化类。

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


0

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


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

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


1
除了cpp避免与实际源文件混淆外,包含的光荣的第二个标头还应至少具有扩展名。常见建议包括tpptcc
underscore_d


0

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

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

        TClass<int> myObj;

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

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


-3

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.