说明:C++11 新特性中的外部模板(Explicit Instantiation Declaration)是一种机制,允许开发者在非模板定义的源文件中明确地声明或定义模板实例。这样做的主要目的是为了避免在多个编译单元中重复编译模板代码,从而减少编译时间和潜在的编译错误。
外部模板的声明告诉编译器,模板的某个具体实例应该在某个特定的编译单元中生成,而不需要在每个包含模板声明的编译单元中都进行实例化。这通常用于库的实现中,以确保模板代码只被编译一次。
// 模板类声明:在头文件中声明需要实例化的模板。
//template.h
template<class T>
class MyClass {
// ...
};
// 模板类定义:在一个编译单元中实际实例化这些模板。
//template.cpp
template<class T>
void MyClass<T>::method() {
// ...
}
// 外部模板实例化声明
// 这告诉编译器在当前编译单元中生成 MyClass<int> 的实例
//在源文件中使用 extern template 关键字来指定需要实例化的模板。
//use_template_demo.cpp
extern template class MyClass<int>;
在这段代码中,MyClass
是一个模板类,我们通过 extern template
关键字在编译单元中声明了 MyClass<int>
的实例化。这样,只有当 MyClass<int>
被实际使用时,编译器才会生成相应的实例化代码。如果没有使用 extern template
声明,编译器将在每个包含模板声明的编译单元中都生成实例化代码,这可能会导致编译效率降低和编译错误。
注意:extern template
声明不会阻止模板的隐式实例化,如果其他编译单元中使用了 MyClass<int>
并且没有看到 extern template
声明,编译器仍然会为它生成实例化代码。因此,extern template
通常与模板的显式实例化一起使用,以确保一致性和避免重复实例化。接下我们完整地解读下C++11为什么要引入外部模板?👇
1 C++11为什么要引入外部模板?
1.1 外部模板解决了什么问题?
C++11 中引入的外部模板(Extern Templates)特性主要解决了以下问题:
-
编译时间的优化:在大型项目中,如果需要频繁使用某些模板实例化,会导致编译时间的显著增加。使用外部模板可以将这些频繁使用的模板实例化延迟到链接时进行,从而大幅减少编译时间。
-
模板实例化的控制:在C++98/03中,编译器会自动实例化所需的模板实例。这可能导致不必要的模板实例化,增加了代码大小和编译时间。外部模板允许程序员明确指定需要实例化的模板,从而更好地控制模板的实例化过程。
-
链接时错误的检测:在C++98/03中,如果一个模板实例化失败,编译器通常会在编译时报告错误,而不是链接时。使用外部模板可以将模板实例化的错误检测推迟到链接时,从而更早地发现问题,并提高编译和链接的可靠性。
-
代码可移植性的增强:在不同编译器或平台上,模板的实例化行为可能不同。这会导致编译或链接错误。使用外部模板可以在一个编译器上预先实例化模板,并在其他编译器上重用这些实例化,提高了代码的可移植性。
总的来说,外部模板是C++11中一个重要的新特性,它可以显著优化编译时间,提高模板使用的灵活性和可控性,并增强代码的可移植性。这对于大型C++项目的开发非常有帮助。
1.2 为什么C++11之前未引入该特性,C++11之后才引入?
之所以C++11之前未引入外部模板特性,而是到了C++11才引入,主要有以下几个原因:
-
技术难度较高:实现外部模板需要编译器和链接器之间进行深度协作,需要解决许多复杂的技术问题。在C++98/03时期,这样的技术难度还比较高。
-
标准化过程较慢:引入新语言特性需要经过标准化委员会的审议讨论,达成共识。在C++98/03时期,标准化过程较为缓慢,直到C++11才最终确定引入外部模板。
-
编译器支持的限制:外部模板依赖编译器和链接器的配合支持。在C++98/03时期,各家编译器对此支持程度有限,难以在标准中引入。
-
使用场景的局限性:在C++98/03时期,大型C++项目还不如现在普遍,使用外部模板的需求相对较小。直到随着C++项目规模的不断增大,这种需求才变得更加迫切。
-
优先级较低:在C++98/03时期,标准化委员会将优先级更高的其他语言特性,如异常处理、命名空间等作为首要任务。外部模板相对来说优先级较低。
到了C++11时期,随着编译器技术的进步,标准化过程的加快,以及大型C++项目的普及,引入外部模板特性的需求终于变得更加迫切和可行。这才使得外部模板最终成为C++11标准的一部分。
总之,外部模板的引入需要克服较高的技术难度和标准化障碍,加之当时的使用场景和优先级问题,在C++98/03时期还难以在标准中引入,直到C++11才最终实现。
2 变长参数模板 使用详解
2.1 优化编译时间
假设我们有一个头文件matrix.h
定义了一个Matrix
类模板,并且在整个项目中被大量使用,代码如下:
// matrix.h
#ifndef MATRIX_H
#define MATRIX_H
template <typename T, int rows, int cols>
class Matrix {
public:
// ... class members and methods
};
#endif
为了优化编译时间,我们可以使用外部模板来显式实例化频繁使用的模板类型:
// matrix.cpp
#include "matrix.h"
// 显式实例化常用的模板类型
extern template class Matrix<double, 3, 3>;
extern template class Matrix<int, 4, 4>;
在其他需要使用这些矩阵类型的地方,我们只需包含matrix.h
头文件即可,而不需要再重复实例化这些模板:
// main.cpp
#include "matrix.h"
int main() {
Matrix<double, 3, 3> m1;
Matrix<int, 4, 4> m2;
// 使用m1和m2
return 0;
}
这样做可以大幅减少编译时间,因为编译器不需要重复实例化这些常用的矩阵模板类型。
2.2 改善错误诊断
假设我们有一个头文件sort.h
定义了一个sort
函数模板,代码如下:
// sort.h
#ifndef SORT_H
#define SORT_H
template <typename T>
void sort(T* begin, T* end) {
// 排序实现
}
#endif
在某个源文件中,我们错误地使用了一个不支持的类型进行排序:
// main.cpp
#include "sort.h"
struct MyStruct {
int value;
// 没有实现operator<
};
int main() {
MyStruct arr[] = {{1}, {2}, {3}};
sort(arr, arr + 3); // 错误: MyStruct没有operator<
return 0;
}
在C++98/03中,编译器会在编译时报告模板实例化失败的错误。但使用外部模板,我们可以将这个错误推迟到链接时:
// sort.cpp
#include "sort.h"
// 显式实例化sort模板
extern template void sort<int>(int* begin, int* end);
extern template void sort<double>(double* begin, double* end);
这样在编译main.cpp
时,编译器不会报告错误,直到链接时才会发现sort<MyStruct>
无法实例化,将编译错误推迟到链接时。这种做法的好处是:
- 在编译单元内,编译速度会更快,因为不需要尝试实例化所有可能用到的模板。
- 错误信息可能会更有意义,因为链接时的错误信息通常比编译时更容易理解。
这会再一定程度上改善错误诊断。
2.3 增强代码可移植性
假设我们有一个跨平台的C++库,其中定义了一个Vector3
类模板,代码如下:
// vector3.h
#ifndef VECTOR3_H
#define VECTOR3_H
template <typename T>
class Vector3 {
public:
// ... class members and methods
};
#endif
为了提高库的可移植性,我们可以在库的一个编译单元中显式实例化常用的Vector3
模板类型:
// vector3.cpp
#include "vector3.h"
// 显式实例化常用的Vector3模板类型
extern template class Vector3<float>;
extern template class Vector3<double>;
这样在其他编译器或平台上使用这个库时,就无需再次实例化这些Vector3
模板,从而提高了代码的可移植性。
2.4 支持动态库的模板类
当我们需要将一个模板类导出为动态库的一部分时,使用外部模板可以帮助我们解决一些问题。假设我们有一个模板类MyClass<T>
,需要作为动态库的一部分提供给其他项目使用,代码如下:
// myclass.h
#ifndef MYCLASS_H
#define MYCLASS_H
template <typename T>
class MyClass {
public:
MyClass(T value) : value(value) {}
T getValue() const { return value; }
private:
T value;
};
#endif // MYCLASS_H
如果不使用外部模板,在构建动态库时,编译器需要为所有可能用到的模板实例化生成代码,这会显著增加库的体积。我们可以使用外部模板来解决这个问题:
// myclass.cpp
#include "myclass.h"
// 显式实例化常用的MyClass模板类型
extern template class MyClass<int>;
extern template class MyClass<double>;
extern template class MyClass<std::string>;
这样在构建动态库时,只有这三种MyClass
模板类型的实现会被包含在库中,而不是所有可能的模板实例化。在使用该动态库的项目中,我们只需包含myclass.h
头文件,就可以直接使用这些预先实例化的MyClass
模板类型:
// main.cpp
#include "myclass.h"
int main() {
MyClass<int> intObj(42);
MyClass<double> doubleObj(3.14);
MyClass<std::string> stringObj("hello");
// 使用这些对象
return 0;
}
这种做法可以有效地减小动态库的体积,同时也不会影响使用者的开发体验。
2.5 支持不同编译器的模板函数
在一些大型C++项目中,我们可能需要支持多种编译器,这可能会导致一些模板函数在不同编译器上的实现略有差异。为了解决这个问题,我们可以使用外部模板。假设我们有一个跨平台的模板函数clamp
用于限制值在一个范围内:
// utils.h
#ifndef UTILS_H
#define UTILS_H
template <typename T>
T clamp(const T& value, const T& min, const T& max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
#endif // UTILS_H
在大多数编译器上,上述实现是正确的。但是,如果遇到一些老旧或非标准的编译器,可能需要对clamp
函数的实现做一些调整。我们可以使用外部模板来解决这个问题:
// utils_gcc.cpp
#include "utils.h"
// 针对GCC编译器的clamp实现
#ifdef __GNUC__
extern template T clamp<T>(const T&, const T&, const T&);
#endif
// utils_msvc.cpp
#include "utils.h"
// 针对MSVC编译器的clamp实现
#ifdef _MSC_VER
extern template T clamp<T>(const T&, const T&, const T&);
#endif
在不同的编译单元中,我们根据编译器的不同,显式实例化对应的clamp
模板函数实现。这样,使用utils.h
头文件的项目就可以根据编译器的不同,链接到正确的clamp
函数实现,而不需要修改自身的代码。这种做法可以大大提高代码的可移植性,并减少维护成本。
通过这些案例,我们可以看到外部模板在处理复杂的模板实现、支持动态库、以及跨编译器兼容性方面都有很好的应用。这是一个非常有价值的C++11特性。