19.3 模板类¶
By Alex on June 16th, 2008 | last modified by Alex on December 21st, 2020
翻译 By dashjay 2020-12-27 | 最后修改于 2020-12-27
在之前的两节课中,你学习了 19.1 -- Function templates 然后实例化成了 19.2 -- Function template instances ,允许我们生成函数来在不同数据类型下工作。尽管这是一个到广义的编程(generalized programming)的很棒的开始,但是它不能解决我们所有的问题。让我们看一看其他问题的例子,看看模板还可以为我们做什么。
模板和容器类¶
在 16.6 -- Container classes 中,你学习了如何复合来实现包含多个其他类的实例的的类。作为一个这样的容器,我们看一眼 IntArray
类。这有个简单的例子:
#ifndef INTARRAY_H
#define INTARRAY_H
#include <cassert>
class IntArray
{
private:
int m_length{};
int *m_data{};
public:
IntArray(int length)
{
assert(length > 0);
m_data = new int[length]{};
m_length = length;
}
// We don't want to allow copies of IntArray to be created.
IntArray(const IntArray&) = delete;
IntArray& operator=(const IntArray&) = delete;
~IntArray()
{
delete[] m_data;
}
void Erase()
{
delete[] m_data;
// We need to make sure we set m_data to 0 here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
int& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
#endif
这个类提供了一个容易的方式来创建一个整型数组,如果你想创建一个 double 类型的数组会发生什么?使用传统的编程方法我们不得不创建一整个新的类!这是一个Double 数组的例子,一个数组用来存储 doubles。
# ifndef DOUBLEARRAY_H
# define DOUBLEARRAY_H
# include <cassert>
class DoubleArray
{
private:
int m_length{};
double *m_data{};
public:
DoubleArray(int length)
{
assert(length > 0);
m_data = new double[length]{};
m_length = length;
}
DoubleArray(const DoubleArray&) = delete;
DoubleArray& operator=(const DoubleArray&) = delete;
~DoubleArray()
{
delete[] m_data;
}
void Erase()
{
delete[] m_data;
// We need to make sure we set m_data to 0 here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
double& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
# endif
尽管代码冗长,你会注意这两个类几乎是一样的!事实上,实质上不同的只是数据类型(int vs double)。你可能会像,这是另一个模板可以生效的地方吧,为了让我们轻松的创建任何数据类型的数组。
创建模板类和模板函数几乎是相同的,因此我们就放一个例子就好。这是一个我们的类模板的版本:
Array.h:
# ifndef ARRAY_H
# define ARRAY_H
# include <cassert>
template <class T>
class Array
{
private:
int m_length{};
T *m_data{};
public:
Array(int length)
{
assert(length > 0);
m_data = new T[length]{};
m_length = length;
}
Array(const Array&) = delete;
Array& operator=(const Array&) = delete;
~Array()
{
delete[] m_data;
}
void Erase()
{
delete[] m_data;
// We need to make sure we set m_data to 0 here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
T& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
// templated getLength() function defined below
int getLength() const;
};
// member functions defined outside the class need their own template declaration
template <class T>
int Array<T>::getLength() const // note class name is Array<T>, not Array
{
return m_length;
}
# endif
如你所见,这个版本和 IntArray
几乎是相同的,除了我们添加了模板声明,并且改变了数据类型从 int
到 T
。
注意我们也在类外定义了 getLength()
函数。这不是必须的,但是新的程序员经常会在第一次这样做的时候失败,因为复杂的语法,因此这个例子可以给你一些启发。每个模板成员函数定义在类外需要它自己的模板定义。同时注意模板数组类是 Array<T>
而不是 Array
—— Array
会引用到没有模板的类版本,除非Array
在类中被使用。例如,拷贝构造函数和拷贝复制操作符就可以使用 Array
而不是 Array<T>
。当类名在类中使用而不带模板参数的时候,参数与当前实例化的参数相同。
有一个短的例子使用上面的模板数组类:
# include <iostream>
# include "Array.h"
int main()
{
Array<int> intArray(12);
Array<double> doubleArray(12);
for (int count{ 0 }; count < intArray.getLength(); ++count)
{
intArray[count] = count;
doubleArray[count] = count + 0.5;
}
for (int count{ intArray.getLength() - 1 }; count >= 0; --count)
std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';
return 0;
}
例子打印如下:
11 11.5
10 10.5
9 9.5
8 8.5
7 7.5
6 6.5
5 5.5
4 4.5
3 3.5
2 2.5
1 1.5
0 0.5
模板类和模板函数一样以同样的方式实例化——编译器在按需拷贝一份,使用实际用户需要的类型来替换模板类型,并且编译这个拷贝。如果你没有在任何地方使用模板类,编译器不会编译它。
模板类是一种理想的容器类的实现,因为让容器类在不同的数据类型上工作是可取的,并且模板允许你无需拷贝代码的情况下这样做。尽管语法有点丑,并且报错信息有点隐晦,模板类真的是 C++ 最好最有用的特性之一。
标准库中的模板类¶
现在我们已经学到了模板类,你需要理解 std::vector<int>
意思是啥 —— std::vector
实际上就是一个模板类,并且 int
是他的模板类型参数!标准库预定义了很多模板类给你用,我们会在稍后的章节中学到这些。
分割模板类¶
一个模板不是一个类或者一个函数 —— 是一个 “漏字板” 用来创建类或者函数。因此,他们和普通的函数或者类工作方式类似,这没啥大问题。然而有一个很常见的问题会给开发者带来问题。
使用废模板类,常见的过程就就是将类定义放在头文件,然后成员函数放在一个名字相似的文件中。以这种方式,类的源代码在分离的项目文件中被编译。然而如果你使用模板,这就不会工作了,看下面的例子:
Array.h:
# ifndef ARRAY_H
# define ARRAY_H
# include <cassert>
template <class T>
class Array
{
private:
int m_length{};
T* m_data{};
public:
Array(int length)
{
assert(length > 0);
m_data = new T[length]{};
m_length = length;
}
Array(const Array&) = delete;
Array& operator=(const Array&) = delete;
~Array()
{
delete[] m_data;
}
void Erase()
{
delete[] m_data;
m_data = nullptr;
m_length = 0;
}
T& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const;
};
# endif
Array.cpp:
# include "Array.h"
template <class T>
int Array<T>::getLength() const // note class name is Array<T>, not Array
{
return m_length;
}
main.cpp:
# include "Array.h"
int main()
{
Array<int> intArray(12);
Array<double> doubleArray(12);
for (int count{ 0 }; count < intArray.getLength(); ++count)
{
intArray[count] = count;
doubleArray[count] = count + 0.5;
}
for (int count{ intArray.getLength() - 1 }; count >= 0; --count)
std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';
return 0;
}
以上程序能通过编译,但是链接器会报错:
unresolved external symbol "public: int __thiscall Array::getLength(void)" (?GetLength@?$Array@H@@QAEHXZ)
为了让编译器使用一个模板,必须要同时能看到模板定义(不仅仅是声明)并且模板类被被用来实例化成了模板。同时记住,C++ 单独编译文件。当 Array.h
头文件在 main
中被 #include
之后,模板类的定义就被拷贝到了 main.cpp
。当编译器发现我们需要模板实例化 Array<int>, Array<double>
的时候,他会实例化这些,并且编译他们作为 main.cpp
。然而,当它绕过单独编译 Array.cpp
的时候,他会忘记我们需要一个 Array<int>
和 Array<double>
,因此模板函数从来不会呗实例化。因此我们得到一个连接错误,因为编译器不能发现 Array<int>::getLength()
或者 Array<double>::getLength()
的定义。
有很多方法来解决这个问题。
最简单的方法就是吧模板类的所有代码都放在头文件里(在这个例子中,吧 Array.cpp
中的内容放进 Array.h
中,在类的声明下方)。以这种方式,当你 #include
这个头文件的时候,所有的模板代码就会在同一个地方。这样解决的优点就是非常简单。缺点就是如果模板类在很多地方被使用的话,你就会最终有很多模板类的本地拷贝,会增加你的编译和连接的耗时(你的连接器会移除重复的定义,因此他不会让可执行程序膨胀)。这是我们的建议做法,除非编译器或者连接器消耗的时间成为问题。
如果你觉得把 Array.cpp
的代码放进 Array.h
头部使得你的头文件太长/乱,一个可选的方案就是将 Array.cpp
命名为 Array.inl
(.inl 代表内联),然后紧接着 在 Array.h
头文件的底部 include Array.inl
。和把代码放到头文件的结果是一样的,但是相对来说更加干净。
其他的解决方案涉及 #including .cpp 文件,但是我们不建议这样做,因为这不是 #include
的标准用法。
其他方法是使用三文件方法。模板类定义在 header 头文件中。模板类成员函数编写在代码文件中。然后你添加一个 “第三方” 文件,将这两个你实例化的类按需包含:
templates.cpp:
// Ensure the full Array template definition can be seen
# include "Array.h"
# include "Array.cpp" // we're breaking best practices here, but only in this one place
// #include other .h and .cpp template definitions you need here
template class Array<int>; // Explicitly instantiate template Array<int>
template class Array<double>; // Explicitly instantiate template Array<double>
// instantiate other templates here
“template class” 让编译器来显式的实例化模板类。在上方的例子中,编译器会同时生成 Array<int>
和 Array<double>
在 template.cpp
中。因为 templates.cpp
在我们的项目中,这个紧接着就会被编译。然后这些函数可以被从其他地方连接到。
这方法更高效,但是需要为每个程序管理 templaes.cpp
文件。