[C++11] 外部模板机制

说明: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)特性主要解决了以下问题:

  1. 编译时间的优化:在大型项目中,如果需要频繁使用某些模板实例化,会导致编译时间的显著增加。使用外部模板可以将这些频繁使用的模板实例化延迟到链接时进行,从而大幅减少编译时间。

  2. 模板实例化的控制:在C++98/03中,编译器会自动实例化所需的模板实例。这可能导致不必要的模板实例化,增加了代码大小和编译时间。外部模板允许程序员明确指定需要实例化的模板,从而更好地控制模板的实例化过程。

  3. 链接时错误的检测:在C++98/03中,如果一个模板实例化失败,编译器通常会在编译时报告错误,而不是链接时。使用外部模板可以将模板实例化的错误检测推迟到链接时,从而更早地发现问题,并提高编译和链接的可靠性。

  4. 代码可移植性的增强:在不同编译器或平台上,模板的实例化行为可能不同。这会导致编译或链接错误。使用外部模板可以在一个编译器上预先实例化模板,并在其他编译器上重用这些实例化,提高了代码的可移植性。

总的来说,外部模板是C++11中一个重要的新特性,它可以显著优化编译时间,提高模板使用的灵活性和可控性,并增强代码的可移植性。这对于大型C++项目的开发非常有帮助。

1.2 为什么C++11之前未引入该特性,C++11之后才引入?

之所以C++11之前未引入外部模板特性,而是到了C++11才引入,主要有以下几个原因:

  1. 技术难度较高:实现外部模板需要编译器和链接器之间进行深度协作,需要解决许多复杂的技术问题。在C++98/03时期,这样的技术难度还比较高。

  2. 标准化过程较慢:引入新语言特性需要经过标准化委员会的审议讨论,达成共识。在C++98/03时期,标准化过程较为缓慢,直到C++11才最终确定引入外部模板。

  3. 编译器支持的限制:外部模板依赖编译器和链接器的配合支持。在C++98/03时期,各家编译器对此支持程度有限,难以在标准中引入。

  4. 使用场景的局限性:在C++98/03时期,大型C++项目还不如现在普遍,使用外部模板的需求相对较小。直到随着C++项目规模的不断增大,这种需求才变得更加迫切。

  5. 优先级较低:在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特性。

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图王大胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值