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高级编程