C++模板编程


643 浏览 5 years, 4 months

2.5 方法模板

版权声明: 转载请注明出处 http://www.codingsoho.com/

方法模板

C++允许模板化类中的单个方法。这些方法可以在一个类模板中,也可以在一个非模板化的类中。在编写一个模板化的类方法时,实际在为很多不同的类型编写很多不同版本的方法。在类模板中,方法模板对赋值运算符和复制构造函数非常有用。

提示: 不能用方法模板编写虚方法和析构函数。

考虑最早只有一个参数—元素类型—的Grid模板。可以实例化很多不同类型的网格,例如int网格和double网格:

    Grid<int> myIntGrid;
    Grid<double> myDoubleGrid;

然而,Grid<int>和Grid<double>是两种不同的类型。如果写一个接受类型为Grid<double>的对象的函数,那么就不能传入一个Grid<int>。即使您知道int网格中的元素可以复制到double网格中(因为int可以强制转换为double),但是也不能将一个类型为Grid<int>的对象赋值给一个类型为Grid<double>的对象,也不能从一个Grid<int>构造一个Grid<double>。

下面两行代码都无法成功编译:

    myDoubleGrid = myIntGrid;
    Grid<double> newDoubleGrid(myIntGrid);

问题在于Grid模板的复制构造函数和operator=的原型如下:

Grid(const Grid<T>& src);
Grid<T>& operator=(const Grid<T>& rhs);

Grid的复制构造函数和operator=都接受一个constGrid<T>的引用。当实例化一个Grid<double>并试图调用复制构造函数和operator=的时候,编译器通过这些原型生成方法代码:

Grid(const Grid<double>& src);
Grid<double>& operator=(const Grid<double>& rhs);

注意,在生成的Grid<double>类中,没有哪一个构造函数或operator=接受一个Grid<int>作为参数。

然而,在Grid类中添加模板化的复制构造函数和operator=之后可以生成将一种网格类型转换为另一种网格类型的例程,从而修复了这个疏漏。下面是新的Grid类定义:

template <typename T>
class Grid
{
public:
    Grid(size_t inWidth = kDefaultWidth, size_t inHeight = kDefaultHeight);
    Grid(const Grid<T>& src);
    template <typename E>
    Grid(const Grid<E>& src);
    virtual ~Grid();
    //
    Grid<T>& operator=(const Grid<T>& rhs);
    template <typename E>
    Grid<T>& operator=(const Grid<E>& rhs);
    // Omitted for brevity
protected:
    void copyFrom(const Grid<T>& src);
    template <typename E>
    void copyFrom(const Grid<E>& src);
    T** mCells;
    size_t mWidth, mHeight;
};

提示:成员模板不会替换同名的非模板成员。由于编译器生成的版本,所以这条规则会导致复制构造函数和oprator=的问题。如果编写模板化版本的复制构造函数和oprator=并忽略非模板化的版本,那么编译器在给同样类型的网格赋值的时候就不会调用这些新的模板化的版本。

相反,编译器会生成一个复制构造函数和一个用于两个同样类型的网格的创建和赋值,而不是按照您想要的方式完成任务!因此,还必须保留老的非模板化的复制构造函数和oprator=。

首先检查新的模板化的复制构造函数:

template <typename E>
Grid(const Grid<E>& src);

可以看到另一个具有不同类型名称E(element的简写)的模板声明。这个类在一个类型T上模板化,这个新的复制构造函数又在另一个不同的类型E上模板化。通过这种双重模板化可将一种类型的网格复制到另一种类型的网格。

下面是新的复制构造函数的定义:

template <typename T>
template <typename E>
Grid<T>::Grid(const Grid<E>& src)
{
    copyFrom(src);
}

从中可以看出,必须将类模板那行的声明(带有T参数)放在成员模板的那一行声明(带有E参数)前面。不能这样合并两者:

template <typename T, typename E> // Incorrect for nested template constructor
Grid<T>::Grid(const Grid<E>& src)
{
    copyFrom(src);
}

这个复制构造函数使用了protected copyFrom()方法,因此这个类还需要一个模板化的copyFrom()版本:

template <typename T>
template <typename E>
void Grid<T>::copyFrom(const Grid<E>& src)
{
    mWidth = src.getWidth();
    mHeight = src.getHeight();
    //
    mCells = new T* [mWidth];
    for (size_t i = 0; i < mWidth; i++) {
        mCells[i] = new T[mHeight];
    }
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            mCells[i][j] = src.getElementAt(i, j);
        }
    }
}

除了copyFrom()方法定义之前额外的模板参数行之外,注意必须通过公共的访问方法getWidth(), getHeight()和getElementAt()访问src中的元素。这是因为复制目标对象的类型为Grid<T>,而复制来源对象的类型为Grid<E>。这两者不是同一类型,因此必须使用公共方法。

最后一个模板化方法是赋值运算符。注意,这个运算符接受一个constGrid<E>&作为参数,但返回一个Grid<T>&:

template <typename T>
template <typename E>
Grid<T>& Grid<T>::operator=(const Grid<E>& rhs)
{
    // free the old memory
    for (size_t i = 0; i < mWidth; i++) {
        delete [] mCells[i];
    }
    delete [] mCells;
    mCells = nullptr;
    // copy the new memory
    copyFrom(rhs);
    return *this;
}

在模板化的赋值运算符中不需要检查自赋值,因为相同类型的赋值仍然是通过老的非模板化版本的operator=进行,因此在这里不可能进行自赋值。

1、带有非类型参数的方法模板

在之前HEIGHT和WIDTH整数模板参数的例子中,可看到一个主要问题是高度和宽度成为类型的一部分。因为存在这个限制,所以不能将个某高度和宽度的网格赋值给另个不同高度和宽度的网格。

然而在某些情况下,需要将某种大小的网格赋值或复制到另一个大小的网格。不一定要把源对象完美地复制到目标对象,可从源数组中只复制那些能够放在目标数组的元素;如果源数组在任何一个维度上比目标数组小,可以用默认值填充。有了赋值运算符和复制构造函数的方法模板之后,完全可以实现这个操作,从而允许不同大小的网格的赋值和复制。下面是类定义:

template <typename T, size_t WIDTH = 10, size_t HEIGHT = 10>
class Grid
{
public:
    // Writing a copy constructor (even a templatized one)
    // prevents the compiler from generating a default constructor
    Grid() {}
    // 
    template <typename E, size_t WIDTH2, size_t HEIGHT2>
    Grid(const Grid<E, WIDTH2, HEIGHT2>& src);
    // 
    template <typename E, size_t WIDTH2, size_t HEIGHT2>
    Grid<T, WIDTH, HEIGHT>& operator=(const Grid<E, WIDTH2, HEIGHT2>& rhs);
    // 
    void setElementAt(size_t x, size_t y, const T& inElem);
    T& getElementAt(size_t x, size_t y);
    const T& getElementAt(size_t x, size_t y) const;
    size_t getHeight() const { return HEIGHT; }
    size_t getWidth() const { return WIDTH; }

protected:
    template <typename E, size_t WIDTH2, size_t HEIGHT2>
    void copyFrom(const Grid<E, WIDTH2, HEIGHT2>& src);
    T mCells[WIDTH][HEIGHT];
};

这个新的定义包含复制构造函数和赋值运算符的方法模板,还包含一个辅助方法copyFrom()。当您编写复制构造函数时,编译器就不会自动生成一个默认的构造函数,因此还必须加上一个默认的构造函数。

不过要注意,不需要编写非模板化版本的复制构造函数和赋值运算符方法,因为编译器依然会生成这些代码。这些方法只是将来源对象的mCells复制或赋值到目标对象的mCells,语义和两个一样大小的网格的语义完全一致。

当您模板化复制构造函数、赋值运算符和copyFrom()时,必须指定所有3个模板参数。下面是模板化的复制构造函数:

template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid<T, WIDTH, HEIGHT>::Grid(const Grid<E, WIDTH2, HEIGHT2>& src)
{
    copyFrom(src);
}

下面是copyFrom()和operator=的实现。注意,copyFrom()从src在x维度和y维度上分别只复制WIDTH和HEIGHT个元素,即使src比WIDTH和HEIGHT指定的大小要大也同样如此。如果src在任何一个维度上比这个指定值小,那么copyFrom()用0初始化的值填充多余的位置。

如果T是一个类类型,那么T()调用对象的默认构造函数,如果T是简单类型,则生成0。这种语法称为0初始化语法。这是一种为不知道类型的变量提供一个合理默认值的好方法:

template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid<T, WIDTH, HEIGHT>& Grid<T, WIDTH, HEIGHT>::operator=(
    const Grid<E, WIDTH2, HEIGHT2>& rhs)
{
    // no need to check for self-assignment because this version of
    // assignment is never called when T and E are the same
    // 
    // No need to free any memory first
    copyFrom(rhs);
    return *this;
}

template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
void Grid<T, WIDTH, HEIGHT>::copyFrom(
    const Grid<E, WIDTH2, HEIGHT2>& src)
{
    for (size_t i = 0; i < WIDTH; i++) {
        for (size_t j = 0; j < HEIGHT; j++) {
            if (i < WIDTH2 && j < HEIGHT2) {
                mCells[i][j] = src.getElementAt(i, j);
            } else {
                mCells[i][j] = T();
            }
        }
    }
}