1 - 模板介绍,类模板与模板实现原理
C++不仅支持面向对象编程,还支持泛型编程(Generic Programming)。泛型编程的目的就是 编写可重用的代码。在C++中,泛型编程的基本工具是模板,模板可与面向对象编程结合使用,产生强大的作用。
本文将简要介绍一下模板及其实现原理,并介绍类模板是怎么写的。
模板简介
面向过程的函数是值参数化的体现,而 模板不仅允许参数化值,还允许参数化类型,这导致模板的语法比较复杂,让人望而生畏。但要想成为专业的C++程序员,模板是必须要掌握的知识。
类模板
类模板定义了一个类,其中,将一些变量的类型、方法的返回类型和方法的参数类型指定为参数。类模板主要用于容器,或用于保存对象的数据结构。
类模板语法
以一个数组类为例,我们定义类模板MyArray.hpp
如下(hpp
是一种特殊的头文件,能在其中编写方法的实现),并逐行分析它的代码结构:
template<typename T>
class MyArray
{
// using比typedef更现代
using iterator = T*;
using const_iterator = const T*;
public:
MyArray(size_t length);
~MyArray();
// 由于声明了析构函数,根据5规则,还要显式声明以下构造函数/赋值运算符:
MyArray(const MyArray& src) = default;
MyArray& operator=(const MyArray& rhs) = default;
MyArray(MyArray&& src) = default;
MyArray& operator=(MyArray&& rhs) = default;
// 顺便重载一下[]
T& operator[] (unsigned index) const
{
return data[index];
}
// 返回开始的地址
iterator begin() const;
const_iterator cbegin() const;
private:
T* data;
};
template<typename T>
MyArray<T>::MyArray(size_t length)
{
if (length > 0)
{
data = new T[length]();
}
else
{
data = nullptr;
}
}
template<typename T>
MyArray<T>::~MyArray()
{
if (data)
{
delete[] data;
}
}
template<typename T>
typename MyArray<T>::iterator MyArray<T>::begin() const
{
return data;
}
template<typename T>
typename MyArray<T>::const_iterator MyArray<T>::cbegin() const
{
return data;
}
首先,我们要定义一个基于类型T
的类模板,通用格式如下:
template<typename T>
class ClassName
{
// ...
};
其中,typename
关键字可被替换为class
(不推荐),它的作用之一就是在定义模板时表示这一特定的类型;然后就像定义类一样继续往下写。
然后我们定义了一个私有数据成员T* data
,它是一个T
类型的传统c风格数组。接着我们定义了它的构造函数和析构函数,以及根据5规则显式声明的移动/拷贝构造函数/赋值运算符。最后我们定义了它的两个成员函数,用于获取第一个元素的地址,注意这里用了using
。
接下来要实现定义好的成员函数,以构造函数为例:
template<typename T>
MyArray<T>::MyArray(size_t length)
{
// ...
}
但当我们实现cbegin()
时却犯难了,用上面的写法找不到using
声明的iterator
,此时typename
关键字便又派上用场,它的另一个作用就是 在类外表明自定义类型,可以这样做:
template<typename T>
typename MyArray<T>::iterator MyArray<T>::begin() const
{
// ...
}
使用类模板
包含该.hpp
文件后,就能正常使用了:
MyArray<int> arrayI(100);
std::cout << *arrayI.begin() << std::endl;
模板实现原理
编译器遇到模板方法定义时,会进行语法检查,但是并不编译模板,因为它暂时还不知道T是什么类型。当它遇到第一个实例化的模板后,例如MyArray<int>
,它才会将模板类定义中的T更换为int
,编译生成需要的类或函数。
选择性实例化
正如上边所说,编译器 只会编译生成需要的类或函数。也就是说,类模板中某些函数可能包含语法错误而无法被检测出来(除了用到的成员函数和虚函数)。
可以通过 显式模板实例化 来强制编译器为所有方法生成代码(虚函数和非虚函数),用来检测代码的语法错误:
// 不能在函数里这样搞
template class MyArray<int>;
// 也试试更复杂的类型,康康有没有出错
template class MyArray<std::string>;
对类型的要求
在编写与类型无关的代码时,必须对这些类型有一些假设:是否可析构、可复制/移动构造等。
如果试图使用一种不支持该模板所有操作的类型对模板进行实例化,编译器将会无法编译,且会有晦涩难懂的报错。
解决方法有两种,第一种是利用选择性实例化的特性;第二种则是用C++20中新增的 概念(concept),它允许编写编译器可以解释和验证的模板参数需求。
同一文件问题
由于模板的编译特性,导致其 声明与实现都必须放在同一个文件中。因为程序在编译期就得知道函数的具体实现过程。
但在C++20中引入模块概念后,可以通过使用它灵活的将代码存放在多文件中。
参考资料
- 飘零的落花 - 现代C++详解
- C++20高级编程