提问人:MainID 提问时间:1/30/2009 最后编辑:πάντα ῥεῖMainID 更新时间:8/26/2023 访问量:771847
为什么模板只能在头文件中实现?
Why can templates only be implemented in the header file?
问:
目前,使用模板的唯一可移植方法是使用内联函数在头文件中实现它们。
为什么会这样?
(澄清:头文件并不是唯一的可移植解决方案。但它们是最方便的便携式解决方案。
答:
尽管标准 C++ 没有这样的要求,但某些编译器要求所有函数和类模板都需要在它们使用的每个翻译单元中可用。实际上,对于这些编译器,模板函数的主体必须在头文件中可用。重复一遍:这意味着这些编译器不允许在非头文件(如 .cpp 文件)中定义它们
有一个导出关键字应该可以缓解这个问题,但它远未达到可移植性。
评论
在将模板实际编译为目标代码之前,编译器需要对模板进行实例化。只有当模板参数已知时,才能实现此实例化。现在想象一个场景,其中模板函数在 中声明,在 中定义并在 中使用。编译时,不一定知道即将到来的编译将需要模板的实例,更不用说是哪个特定实例了。对于更多的头文件和源文件,情况很快就会变得更加复杂。a.h
a.cpp
b.cpp
a.cpp
b.cpp
有人可能会争辩说,编译器可以更聪明地“展望”模板的所有用途,但我相信创建递归或其他复杂的场景并不难。AFAIK,编译器不会做这样的展望。正如 Anton 所指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(还?
评论
注意:没有必要将实现放在头文件中,请参阅本答案末尾的替代解决方案。
无论如何,代码失败的原因是,在实例化模板时,编译器使用给定的模板参数创建一个新类。例如:
template<typename T>
struct Foo
{
T bar;
void doSomething(T param) {/* do stuff using T */}
};
// somewhere in a .cpp
Foo<int> f;
读取此行时,编译器将创建一个新类(我们称之为 ),它等效于以下内容:FooInt
struct FooInt
{
int bar;
void doSomething(int param) {/* do stuff using int */}
};
因此,编译器需要能够访问方法的实现,以使用 template 参数(在本例中)实例化它们。如果这些实现不在标头中,则无法访问它们,因此编译器将无法实例化模板。int
常见的解决方案是在头文件中编写模板声明,然后在实现文件(例如 .tpp)中实现该类,并将此实现文件包含在标头的末尾。
Foo.h的
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
Foo.tpp(福.tpp)
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
这样,实现仍然与声明分离,但编译器可以访问。
替代解决方案
另一种解决方案是将实现分开,并显式实例化所需的所有模板实例:
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++ 超级常见问题解答。
评论
这意味着定义模板类的方法实现的最可移植方法是在模板类定义中定义它们。
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
实际上,在 C++11 之前,该标准定义了一个关键字,该关键字可以在头文件中声明模板并在其他地方实现它们。在某种程度上。并非如此,正如唯一实现该功能的人所指出的那样:export
幻影优势#1:隐藏源代码。许多用户表示,他们希望通过使用导出,他们将 不再需要为类的成员/非成员函数模板和成员函数提供定义 模板。事实并非如此。通过导出,库编写者仍然必须提供完整的模板源代码或其直接 等效(例如,特定于系统的解析树),因为实例化需要完整的信息。[...]
Phantom 优势 #2:快速构建,减少依赖。许多用户希望导出将允许真正的分离 将模板编译为目标代码,他们希望这将允许更快的构建。它不是因为 导出模板的编译确实是分开的,但不是目标代码。相反,导出几乎总是使 构建速度较慢,因为在预链接时仍必须完成至少相同数量的编译工作。出口 甚至不会减少模板定义之间的依赖关系,因为依赖关系是固有的, 独立于文件组织。
没有一个流行的编译器实现这个关键字。该功能的唯一实现是在 Edison Design Group 编写的前端中,由 Comeau C++ 编译器使用。所有其他方法都要求您在头文件中编写模板,因为编译器需要模板定义才能正确实例化(正如其他人已经指出的那样)。
因此,ISO C++ 标准委员会决定在 C++11 中删除模板的功能。export
评论
export
这里有很多正确的答案,但我想添加这个(为了完整):
如果在实现 cpp 文件的底部,对模板将使用的所有类型进行显式实例化,则链接器将能够像往常一样找到它们。
编辑:添加显式模板实例化的示例。在定义模板并定义所有成员函数后使用。
template class vector<int>;
这将实例化(从而可供链接器使用)类及其所有成员函数(仅)。类似的语法也适用于函数模板,因此,如果您有非成员运算符重载,则可能需要对这些重载执行相同的操作。
上面的例子是相当无用的,因为 vector 是在头文件中完全定义的,除非使用一个通用的包含文件(预编译的头文件?)来防止它在所有其他 (1000?) 使用 vector 的文件中实例化它。extern template class vector<int>
评论
type
vector
不是一个很好的例子,因为容器本质上是面向“所有”类型的。但是,您创建的模板确实经常用于一组特定的类型,例如数字类型:int8_t、int16_t、int32_t、uint8_t、uint16_t等。在这种情况下,使用模板仍然有意义,但为整个类型集显式实例化它们也是可能的,在我看来,建议这样做。
.cpp
.cpp
这是完全正确的,因为编译器必须知道它的分配类型。所以模板类、函数、枚举等。如果要将其公开或作为库的一部分(静态或动态),也必须在头文件中实现,因为头文件不会像 c/cpp 文件那样编译。如果编译器不知道类型,则无法编译它。在 .Net 中,它可以,因为所有对象都派生自 Object 类。这不是 .Net。
评论
这是因为需要单独编译,并且模板是实例化样式的多态性。
让我们更接近具体一点来解释一下。假设我有以下文件:
- foo.h
- 声明
class MyClass<T>
- 声明
- foo.cpp
- 定义了
class MyClass<T>
- 定义了
- 条.cpp
- 使用
MyClass<int>
- 使用
单独编译意味着我应该能够独立于 bar.cpp 编译 foo.cpp。编译器完全独立地在每个编译单元上完成分析、优化和代码生成的所有艰苦工作;我们不需要做整个程序分析。只有链接器需要一次处理整个程序,链接器的工作要容易得多。
当我编译 foo.cpp 时,bar.cpp 甚至不需要存在,但我仍然应该能够将我已经拥有的 foo.o 与我刚刚生成的 bar.o 链接在一起,而无需重新编译 foo.cpp。foo.cpp甚至可以被编译成一个动态库,在没有foo.cpp的情况下分发到其他地方,并与我在我编写foo.cpp多年后编写的代码链接。
“实例化样式多态性”意味着模板实际上不是一个泛型类,可以编译为可以处理任何值的代码。这将增加开销,例如装箱、需要将函数指针传递给分配器和构造函数等。C++ 模板的目的是避免编写几乎相同的 、 等,但仍然能够最终得到编译的代码,就好像我们单独编写了每个版本一样。因此,模板实际上就是一个模板;类模板不是一个类,它是为我们遇到的每个类创建一个新类的秘诀。模板不能编译成代码,只能编译实例化模板的结果。MyClass<T>
T
class MyClass_int
class MyClass_float
T
因此,当编译 foo.cpp 时,编译器无法看到 bar.cpp 来知道需要这样做。它可以看到模板,但不能为此发出代码(它是一个模板,而不是一个类)。当 bar.cpp 被编译时,编译器可以看到它需要创建一个 ,但它看不到模板(只有它在 foo.h 中的接口),所以它不能创建它。MyClass<int>
MyClass<T>
MyClass<int>
MyClass<T>
如果 foo.cpp 本身使用 ,则在编译 foo.cpp 时将生成其代码,因此当 bar.o 链接到 foo.o 时,它们可以被连接并工作。我们可以利用这一事实,通过编写单个模板,允许在.cpp文件中实现一组有限的模板实例化。但是 bar.cpp 无法将模板用作模板并在它喜欢的任何类型上实例化它;它只能使用 FOO.cpp 的作者认为提供的模板化类的预先存在的版本。MyClass<int>
您可能会认为,在编译模板时,编译器应该“生成所有版本”,并在链接过程中过滤掉从未使用的版本。除了巨大的开销和这种方法将面临的极端困难之外,因为指针和数组等“类型修饰符”功能甚至只允许内置类型产生无限数量的类型,当我现在通过添加以下内容来扩展我的程序时会发生什么:
- baz.cpp
- 声明和实现,并使用
class BazPrivate
MyClass<BazPrivate>
- 声明和实现,并使用
除非我们
- 每次我们更改程序中的任何其他文件时都必须重新编译 foo.cpp,以防它添加了一个新的新实例化
MyClass<T>
- 要求 baz.cpp 包含(可能通过标头包含)的完整模板,以便编译器可以在编译 baz.cpp 期间生成。
MyClass<T>
MyClass<BazPrivate>
没有人喜欢(1),因为全程序分析编译系统需要很长时间才能编译,并且因为它使得没有源代码就无法分发编译库。所以我们有 (2) 代替。
评论
.tpp
#include
.tpp
模板通常用于标头中,因为编译器需要实例化不同版本的代码,具体取决于模板参数的给定/推导参数,并且(作为程序员)更容易让编译器多次重新编译相同的代码并在以后删除重复数据。
请记住,模板并不直接表示代码,而是该代码的多个版本的模板。
当您在文件中编译非模板函数时,您正在编译一个具体的函数/类。
模板则不然,模板可以用不同的类型实例化,即用具体类型替换模板参数时,必须发出具体代码。.cpp
关键字有一个功能,旨在用于单独编译。
该功能在 AFAIK 中已弃用,只有一个编译器实现了它。
您不应该使用 .
单独编译是不可能的,但也许在 中,如果概念进入,我们可以有某种单独编译的方法。export
export
C++11
export
C++
C++11
C++17
为了实现单独的编译,必须可以进行单独的模板正文检查。 似乎可以通过概念找到解决方案。 看看这篇论文最近在 标准委员会会议。 我认为这不是唯一的要求,因为您仍然需要在用户代码中实例化模板代码的代码。
模板的单独编译问题 我想这也是迁移到模块时出现的问题,目前正在处理中。
编辑:截至2020年8月,模块已经成为C++的现实:https://en.cppreference.com/w/cpp/language/modules
评论
尽管上面有很多很好的解释,但我缺少一种将模板分为标题和正文的实用方法。
我主要关心的是,当我更改其定义时,避免重新编译所有模板用户。
在模板正文中包含所有模板实例对我来说不是一个可行的解决方案,因为模板作者可能不知道它的用法是否全部,模板用户可能无权修改它。
我采用了以下方法,这种方法也适用于较旧的编译器(gcc 4.3.4,aCC A.03.13)。
对于每个模板用法,在其自己的头文件(从 UML 模型生成)中都有一个 typedef。它的主体包含实例化(最终进入一个库,该库在最后链接)。
模板的每个用户都包含该头文件并使用 typedef。
示意图示例:
我的模板.h:
#ifndef MyTemplate_h
#define MyTemplate_h 1
template <class T>
class MyTemplate
{
public:
MyTemplate(const T& rt);
void dump();
T t;
};
#endif
我的模板.cpp:
#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;
}
MyInstantiatedTemplate.h:
#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"
typedef MyTemplate< int > MyInstantiatedTemplate;
#endif
MyInstantiatedTemplate.cpp:
#include "MyTemplate.cpp"
template class MyTemplate< int >;
main.cpp:
#include "MyInstantiatedTemplate.h"
int main()
{
MyInstantiatedTemplate m(100);
m.dump();
return 0;
}
这样,只需要重新编译模板实例化,而不是所有模板用户(和依赖项)。
评论
MyInstantiatedTemplate.h
MyInstantiatedTemplate
当您在编译步骤中使用模板时,编译器将为每个模板实例化生成代码。 在编译和链接过程中,.cpp 文件被转换为纯对象或机器代码,其中包含引用或未定义的符号,因为主 .cpp 中包含的 .h 文件尚未实现。这些文件已准备好与另一个目标文件链接,该文件定义了模板的实现,因此您拥有完整的 a.out 可执行文件。
但是,由于模板需要在编译步骤中进行处理,以便为您定义的每个模板实例化生成代码,因此简单地将模板与其头文件分开编译是行不通的,因为它们总是齐头并进,因为每个模板实例化实际上都是一个全新的类。在常规类中,您可以将 .h 和 .cpp 分开,因为 .h 是该类的蓝图,而 .cpp 是原始实现,因此可以定期编译和链接任何实现文件,但是使用模板 .h 是类应该如何外观的蓝图,而不是对象应该如何外观,这意味着模板 .cpp 文件不是类的原始常规实现, 它只是一个类的蓝图,所以 .h 模板文件的任何实现都无法编译,因为你需要一些具体的东西来编译,从这个意义上说,模板是抽象的。
因此,模板从不单独编译,只有在其他源文件中有具体实例化的地方才会编译。但是,具体实例化需要知道模板文件的实现,因为简单地修改在 .h 文件中使用具体类型是无法完成这项工作的,因为有什么.cpp可以链接,我以后找不到它,因为记住模板是抽象的,无法编译,所以我现在被迫给出实现,所以我知道要编译和链接什么, 现在我有了实现,它被链接到封闭的源文件中。基本上,当我实例化一个模板时,我需要创建一个全新的类,如果我不知道该类在使用我提供的类型时应该是什么样子,我就无法做到这一点,除非我通知模板实现的编译器,所以现在编译器可以替换为我的类型并创建一个可以编译和链接的具体类。typename T
T
总而言之,模板是类应该如何外观的蓝图,类是对象应该如何外观的蓝图。 我无法将模板与具体实例化分开编译,因为编译器只编译具体类型,换句话说,至少在 C++ 中,模板是纯语言抽象。可以说,我们必须对模板进行去抽象化,我们通过给它们一个具体的类型来处理,这样我们的模板抽象就可以转换为常规的类文件,反过来,它就可以正常编译。将模板 .h 文件和模板 .cpp 文件分开是没有意义的。这是荒谬的,因为只有 .cpp 和 .h 才能单独编译和链接 .cpp,因为我们不能单独编译它们,因为模板是一种抽象,因此我们总是被迫将抽象始终与具体实例化放在一起,其中具体实例化始终必须知道正在使用的类型。
这意味着 get 在编译步骤中被替换,而不是链接步骤,因此,如果我尝试编译模板而不被替换为对编译器完全没有意义的具体值类型,因此无法创建对象代码,因为它不知道是什么。typename T
T
T
从技术上讲,可以创建某种功能来保存 template.cpp 文件并在其他来源中找到类型时切换类型,我认为该标准确实有一个关键字,允许您将模板放在单独的 cpp 文件中,但实际上没有多少编译器实现这一点。export
顺便说一句,在为模板类进行专用化时,您可以将标头与实现分开,因为根据定义,专用化意味着我专注于可以单独编译和链接的具体类型。
如果担心的是将 .h 编译为使用它的所有 .cpp 模块的一部分而产生的额外编译时间和二进制大小膨胀,则在许多情况下,您可以做的是使模板类从接口的非类型依赖部分的非模板化基类派生,并且该基类可以在 .cpp 文件中实现。
评论
class XBase
template class X
X
XBase
单独实现的方法如下。
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
我这样做是因为代码的静态分析器在看不到类的正向声明时会中断。在任何 IDE 中编写代码或使用 YouCompleteMe 或其他设备时,这很烦人。*.tpp
评论
.tpp
.ft
.cpp
只是为了在这里添加一些值得注意的东西。当模板化类的方法不是函数模板时,可以在实现文件中很好地定义它们。
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;
...
}
评论
isEmpty
myQueue.cpp
我必须编写一个模板类,这个例子对我有用
下面是动态数组类的示例。
#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);
在实用程序.cpp中:
#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
return one < other ? one : other;
}
这要求这里的每个 T 类都实现小于运算符 (<)。当您比较两个尚未实现“<”的类实例时,它将抛出编译器错误。
因此,如果将模板声明和定义分开,则无法仅读取头文件来查看此模板的来龙去脉,以便在自己的类上使用此 API,尽管编译器会在这种情况下告诉您需要重写哪个运算符。
我建议查看此 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
这在许多编译器中得到了支持。但是,我认为使用模板头文件预编译头文件会很困难。
Moshe 的回答来自:https://stackoverflow.com/a/38448106/6459849
我这边的一个小贡献,有一个扩展的例子。假设有一个整体的 OperationSuccess,它包含一个 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;
};
操作成功.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();
(从封闭的副本复制此处)
我更喜欢将我的所有函数都放在文件中,无论它们是模板函数还是常规函数。有一种方法可以用一些基本的魔法来做到这一点。您可以采取以下措施:.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
我的类.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 时,我们:
- 包括
myclass.hpp
- 检查未定义,它是
MYCLASS
- 定义它
- 为编译器提供生成的类的定义(来自模板类)
- 包括
myclass.cpp
- 定义
MYCLASS_FUNCTIONS
- 检查是否已定义,它是
MYCLASS_FUNCTIONS
- 为编译器提供生成函数的定义(来自模板函数)
- 当我们编译 myclass.cpp
- 检查是否已定义,否则未定义
MYCLASS_FUNCTIONS
- 包括
myclass.hpp
- 检查未定义,它是
MYCLASS
- 定义它
- 为编译器提供类的定义
- 包括
myclass.cpp
- 再次包含
myclass.hpp
- 这个时间是定义的,所以什么都不做,回到
MYCLASS
myclass.cpp
- 检查是否已定义,它是
MYCLASS_FUNCTIONS
- 为编译器提供生成函数的定义(来自模板函数)
- 退出包含两次
- 将所有常规函数传递给编译器
评论