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 */}
};
int
)来实例化它们。如果这些实现不在头文件中,它们将无法访问,因此编译器将无法实例化模板。Foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
Foo.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
MyClass<int>
。它可以看到模板 MyClass<T>
,但无法为其生成代码(它是一个模板,不是类)。当编译 bar.cpp 时,编译器可以看到它需要创建一个 MyClass<int>
,但无法看到模板 MyClass<T>
(只能看到其在 foo.h 中的接口),因此无法创建它。MyClass<int>
,那么编译 foo.cpp 时将生成该代码,因此当将 bar.o 链接到 foo.o 时,它们就可以连接并正常工作。我们可以利用这个事实,通过编写单个模板来实现有限集合的模板实例化。但是,bar.cpp 没有办法使用模板作为模板,并在任何类型上进行实例化;它只能使用foo.cpp 的作者提供的预先存在的模板类的版本。class BazPrivate
,并使用 MyClass<BazPrivate>
MyClass<T>
的新实例化。MyClass<T>
的完整模板,以便编译器可以在编译 baz.cpp 时生成 MyClass<BazPrivate>
。.tpp
文件只是一种头文件的命名规范。"头文件" 并不是特定于 C++ 编译器的东西,它只是我们称呼打算通过使用 #include
包含到其他编译单元的文件。如果将模板实现放在一个与 .cpp
文件描述接口不同的文件中,并给这些模板实现文件一个特定的扩展名(如 .tpp
)可以帮助你处理代码,那就这样做吧!编译器不知道或关心这种区别,但它对人类有所帮助。 - Ben这里有很多正确的答案,但是我想补充一下(为了完整性):
如果你在实现cpp文件的底部对模板将要使用的所有类型进行显式实例化,链接器将像往常一样能够找到它们。
编辑:添加显式模板实例化示例。在定义模板和所有成员函数之后使用。
template class vector<int>;
这将实例化(并使该类及其成员函数可供链接器使用)。 函数模板也有类似的语法,因此如果您有非成员运算符重载,则可能需要对它们执行相同的操作。
上述示例相当无用,因为向量在头文件中完全定义,除非一个常见的包含文件(预编译头?)使用extern template class vector<int>
以避免在使用向量的所有其他(1000?)文件中实例化它。
.cpp
文件中,这两个实例化被其他 .cpp
文件引用,但我仍然得到链接错误,说找不到成员。 - oarfish在将模板编译为目标代码之前,编译器需要先实例化模板。只有当模板参数已知时才能进行实例化。假设有这样一种情况:在a.h
中声明了一个模板函数,在a.cpp
中定义该函数并在b.cpp
中使用它。当编译a.cpp
时,并不确定是否需要实例化模板,更不知道具体的实例是哪个。对于更多的头文件和源文件,情况会变得更加复杂。
有人可以提出,编译器可以变得更智能,预见到所有使用该模板的情况。但是我相信很容易创建递归或其他复杂的场景,让编译器难以预测。据我的了解,编译器并不会进行这种先见性的查找。正如Anton所指出的,一些编译器支持显式导出模板实例的声明,但并非所有编译器都支持(尚未?)。
export
关键字,可以在头文件中声明模板并在其他地方实现它们。某种程度上是这样。但实际上不是这样的,因为唯一一个指出该特性的人是:
幻影优势#1:隐藏源代码。许多用户表示,他们希望使用导出功能后,将不再需要为成员/非成员函数模板和类模板的成员函数提供定义。但这是不正确的。使用导出功能,库编写者仍然必须提供完整的模板源代码或其直接等效物(例如,系统特定的解析树),因为完整信息是实例化所必需的。[...]
幻影优势#2:快速构建、减少依赖。许多用户期望导出功能将允许真正将模板分别编译为目标代码,从而实现更快的构建。但实际上并不是这样的,因为导出模板的编译确实是分开的,但不是编译为目标代码。相反,导出几乎总是使构建变慢,因为至少仍需要在预链接时间完成同样数量的编译工作。导出甚至不能减少模板定义之间的依赖性,因为这些依赖性是内在的,独立于文件组织。
没有任何流行的编译器实现了这个关键字。该特性的唯一实现是由Edison Design Group编写的前端,它被Comeau C++编译器使用。所有其他编译器都要求您在头文件中编写模板,因为编译器需要模板定义以进行正确的实例化(正如其他人已经指出的那样)。
因此,ISO C++标准委员会决定在C++11中删除模板的export
特性。export
实际上给了我们什么,以及没有给我们什么...现在,我从心底赞同EDG的人们:它不会带给我们大多数人(包括我在'11年时)认为的东西,C++标准没有它更好。 - DevSolar尽管标准C++没有这样的要求,但一些编译器需要在每个使用到函数和类模板的翻译单元中都提供它们。因此,在这些编译器中,模板函数的实现必须在头文件中提供。需要强调的是,这意味着这些编译器不允许将其定义在非头文件(例如.cpp文件)中。
有一个“export”关键字可以缓解这个问题,但它远远不能做到可移植。
模板经常用在头文件中,因为编译器需要根据传递/推断的模板参数实例化不同版本的代码,并且让编译器多次重新编译相同的代码并稍后去重更容易(作为程序员)。
请记住,模板不直接代表代码,而是代表该代码的几个版本的模板。
在 .cpp
文件中编译非模板函数时,您正在编译一个具体的函数/类。但对于模板来说情况并非如此,因为它可以使用不同类型进行实例化,即替换模板参数与具体类型时必须发射具体代码。
export
关键字有一个功能,用于分离编译。 export
功能在 C++11
中已被弃用,并且仅有一种编译器实现了它。不应使用 export
。在 C++
或 C++11
中无法实现单独的编译,但在 C++17
中,如果概念得以实现,我们可能会有一些方法实现单独的编译。
要实现单独的编译,必须能够进行单独的模板主体检查,似乎可以通过概念解决。请参阅最近在标准委员会会议上提出的 纸。我认为这不是唯一的要求,因为您仍然需要在用户代码中实例化模板代码。
我猜分离编译问题也是随着迁移到模块而出现的问题,目前正在努力解决它。
编辑:截至 2020 年 8 月,模块已成为 C++ 的现实:https://en.cppreference.com/w/cpp/language/modules
#ifndef MyTemplate_h
#define MyTemplate_h 1
template <class T>
class MyTemplate
{
public:
MyTemplate(const T& rt);
void dump();
T t;
};
#endif
MyTemplate.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
类型之外。如果不使用它,会更加简洁。请查看我在另一个问题上的答案,展示了这一点:https://dev59.com/bHVD5IYBdhLWcg3wBWzd#41292751 - Cameron Tacklind这意味着在模板类定义内部定义方法实现是最具可移植性的方式。
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
typename T
,而不是在链接过程中。因此,如果我尝试编译一个模板,而T
没有被替换为一个具体的值类型,那对于编译器来说就是毫无意义的,结果就是无法创建对象代码,因为它不知道T
是什么。
从技术上讲,可以创建某种功能,当发现其他源中的类型时,会保存template.cpp文件并切换类型,我认为标准确实有一个关键字export
,它将允许您将模板放在单独的cpp文件中,但实际上并没有太多编译器实现这一点。
顺便提一下,当为模板类制作专门化时,可以将头文件与实现分开,因为根据定义,专门化意味着我正在为可以单独编译和链接的具体类型进行专门化。