Skip to content

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 几乎是相同的,除了我们添加了模板声明,并且改变了数据类型从 intT

注意我们也在类外定义了 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 文件。