Наш класс IntArray служит хорошей альтернативой встроенному массиву целых чисел. Но в жизни могут потребоваться массивы для самых разных типов данных. Можно предположить, что единственным отличием массива элементов типа double от нашего является тип данных в объявлениях, весь остальной код совпадает буквально.
Для решения данной проблемы в С++ введен механизм шаблонов. В объявлениях классов и функций допускается использование параметризованных типов. Типы-параметры заменяются в процессе компиляции настоящими типами, встроенными или определенными пользователем. Мы можем создать шаблон класса Array, заменив в классе IntArray тип элементов int на обобщенный тип-параметр. Позже мы конкретизируем типы-параметры, подставляя вместо них реальные типы int, double и string. В результате появится способ использовать эти конкретизации так, как будто мы на самом деле определили три разных класса для этих трех типов данных.
Вот как может выглядеть шаблон класса Array:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
template <class elemType> class Array { public: explicit Array( int sz = DefaultArraySize ); Array( const elemType *ar, int sz ); Array( const Array &iA ); virtual ~Array() { delete[] _ia; } Array& operator=( const Array & ); int size() const { return _size; } virtual elemType& operator[]( int ix ) { return _ia[ix]; } virtual void sort( int,int ); virtual int find( const elemType& ); virtual elemType min(); virtual elemType max(); protected: void init( const elemType*, int ); void swap( int, int ); static const int DefaultArraySize = 12; int _size; elemType *_ia; }; |
Ключевое слово template говорит о том, что задается шаблон, параметры которого заключаются в угловые скобки (<>). В нашем случае имеется лишь один параметр elemType; ключевое слово class перед его именем сообщает, что этот параметр представляет собой тип.
При конкретизации класса-шаблона Array параметр elemType заменяется на реальный тип при каждом использовании, как показано в примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> #include "Array.h" int main() { const int array_size = 4; // elemType заменяется на int Array<int> ia(array_size); // elemType заменяется на double Array<double> da(array_size); // elemType заменяется на char Array<char> ca(array_size); int ix; for ( ix = 0; ix < array_size; ++ix ) { ia[ix] = ix; da[ix] = ix * 1.75; ca[ix] = ix + 'a'; } for ( ix = 0; ix < array_size; ++ix ) cout << "[ " << ix << " ] ia: " << ia[ix] << "\tca: " << ca[ix] << "\tda: " << da[ix] << endl; return 0; } |
Здесь определены три экземпляра класса Array:
1 2 3 |
Array<int> ia(array_size); Array<double> da(array_size); Array<char> ca(array_size); |
Что делает компилятор, встретив такое объявление? Подставляет текст шаблона Array, заменяя параметр elemType на тот тип, который указан в каждом конкретном случае. Следовательно, объявления членов приобретают в первом случае такой вид:
1 2 3 |
// Array<int> ia(array_size); int _size; int *_ia; |
Заметим, что это в точности соответствует определению массива IntArray.
Для оставшихся двух случаев мы получим следующий код:
1 2 3 4 5 6 7 |
// Array<double> da(array_size); int _size; double *_ia; // Array<char> ca(array_size); int _size; char *_ia; |
Что происходит с функциями-членами? В них тоже тип-параметр elemType заменяется на реальный тип, однако компилятор не конкретизирует те функции, которые не вызываются в каком-либо месте программы.
При выполнении программа этого примера выдаст следующий результат:
1 2 3 4 |
[ 0 ] ia: 0 ca: a da: 0 [ 1 ] ia: 1 ca: b da: 1.75 [ 2 ] ia: 2 ca: c da: 3.5 [ 3 ] ia: 3 ca: d da: 5.25 |
Механизм шаблонов можно использовать и в наследуемых классах. Вот как выглядит определение шаблона класса ArrayRC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <cassert> #include "Array.h" template <class elemType> class ArrayRC : public Array<elemType> { public: ArrayRC( int sz = DefaultArraySize ) : Array<elemType>( sz ) {} ArrayRC( const ArrayRC& r ) : Array<elemType>( r ) {} ArrayRC( const elemType *ar, int sz ) : Array<elemType>( ar, sz ) {} elemType& ArrayRC<elemType>::operator[]( int ix ) { assert( ix >= 0 && ix < Array<elemType>::_size ); return _ia[ ix ]; } private: // ... }; |
Подстановка реальных параметров вместо типа-параметра elemType происходит как в базовом, так и в производном классах.
1 |
ArrayRC<int> ia_rc(10); |
Определение ведет себя точно так же, как определение IntArrayRC из предыдущего раздела. Изменим пример использования из предыдущего раздела.
1 |
swap( ia1, 1, ia1.size() ); |
Прежде всего, чтобы оператор // функцию swap() тоже следует сделать шаблоном был допустимым, нам потребуется представить функцию swap() в виде шаблона.
1 2 3 4 5 6 7 8 9 |
#include "Array.h" template <class elemType> inline void swap( Array<elemType> &array, int i, int j ) { elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp; } |
При каждом вызове swap() генерируется подходящая конкретизация, которая зависит от типа массива. Вот как выглядит программа, использующая шаблоны Array и ArrayRC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> #include "Array.h" #include "ArrayRC.h" template <class elemType> inline void swap( Array<elemType> &array, int i, int j ) { elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp; } int main() { Array<int> ia1; ArrayRC<int> ia2; cout << "swap() with Array<int> ia1" << endl; int size = ia1.size(); swap( ia1, 1, size ); cout << "swap() with ArrayRC<int> ia2" << endl; size = ia2.size(); swap( ia2, 1, size ); return 0; } |