Использование шаблонов
Наш класс IntArray
служит хорошей альтернативой встроенному массиву целых чисел. Но в жизни могут потребоваться массивы для самых разных типов данных. Можно предположить, что единственным отличием массива элементов типа double от нашего является тип данных в объявлениях, весь остальной код совпадает буквально.
Для решения данной проблемы в С++ введен механизм шаблонов. В объявлениях классов и функций допускается использование параметризованных типов. Типы-параметры заменяются в процессе компиляции настоящими типами, встроенными или определенными пользователем. Мы можем создать шаблон класса Array, заменив в классе IntArray тип элементов int на обобщенный тип-параметр. Позже мы конкретизируем
типы-параметры, подставляя вместо них реальные типы int, double и string. В результате появится способ использовать эти конкретизации так, как будто мы на самом деле определили три разных класса для этих трех типов данных.
Вот как может выглядеть шаблон класса Array:
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
заменяется на реальный тип при каждом использовании, как показано в примере:
#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:
Array<int> ia(array_size); |
Array<char> ca(array_size);
Что делает компилятор, встретив такое объявление? Подставляет текст шаблона Array, заменяя параметр elemType на тот тип, который указан в каждом конкретном случае. Следовательно, объявления членов приобретают в первом случае такой вид:
// Array<int> ia(array_size);
int _size;
int *_ia;
Заметим, что это в точности соответствует определению массива IntArray.
Для оставшихся двух случаев мы получим следующий код:
// Array<double> da(array_size); |
double *_ia;
// Array<char> ca(array_size);
int _size;
char *_ia;
Что происходит с функциями-членами? В них тоже тип-параметр elemType заменяется на реальный тип, однако компилятор не конкретизирует те функции, которые не вызываются в каком-либо месте программы. (Подробнее об этом в разделе 16.8.)
При выполнении программа этого примера выдаст следующий результат:
[ 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:
#include <cassert> |
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 происходит как в базовом, так и в производном классах. Определение
ArrayRC<int> ia_rc(10);
ведет себя точно так же, как определение IntArrayRC из предыдущего раздела. Изменим пример использования из предыдущего раздела. Прежде всего, чтобы оператор
// функцию swap() тоже следует сделать шаблоном |
был допустимым, нам потребуется представить функцию swap() в виде шаблона.
#include "Array.h" |
inline void
swap( Array<elemType> &array, int i, int j )
{
elemType tmp = array[ i ];
array[ i ] = array[ j ];
array[ j ] = tmp;
}
При каждом вызове swap()
генерируется подходящая конкретизация, которая зависит от типа массива. Вот как выглядит программа, использующая шаблоны Array и ArrayRC:
#include <iostream> |
#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;
}
Упражнение 2.13
Пусть мы имеем следующие объявления типов:
template<class elemType> class Array; |
typedef string *Pstring;
Есть ли ошибки в приведенных ниже описаниях объектов?
(a) Array< int*& > pri(1024); (b) Array< Array<int> > aai(1024); (c) Array< complex< double > > acd(1024); (d) Array< Status > as(1024); |
Упражнение 2.14
Перепишите следующее определение, сделав из него шаблон класса:
class example1 { public: example1 (double min, double max); example1 (const double *array, int size); double& operator[] (int index); bool operator== (const example1&) const; bool insert (const double*, int); bool insert (double); double min (double) const { return _min; }; double max (double) const { return _max; }; void min (double); void max (double); int count (double value) const; private: int size; double *parray; double _min; double _max; |
Упражнение 2.15
Имеется следующий шаблон класса:
template <class elemType> class Example2 { |
explicit Example2 (elemType val=0) : _val(val) {};
bool min(elemType value) { return _val < value; }
void value(elemType new_val) { _val = new_val; }
void print (ostream &os) { os << _val; }
private:
elemType _val;
}
template <class elemType>
ostream& operator<<(ostream &os,const Example2<elemType> &ex)
{ ex.print(os); return os; }
Какие действия вызывают следующие инструкции?
(a) Example2<Array<int>*> ex1; (b) ex1.min (&ex1); (c) Example2<int> sa(1024),sb; (d) sa = sb; (e) Example2<string> exs("Walden"); |
Упражнение 2.16
Пример из предыдущего упражнения накладывает определенные ограничения на типы данных, которые могут быть подставлены вместо elemType. Так, параметр конструктора имеет по умолчанию значение 0:
explicit Example2 (elemType val=0) : _val(val) {};
Однако не все типы могут быть инициализированы нулем (например, тип string), поэтому определение объекта
Example2<string> exs("Walden");
является правильным, а
Example2<string> exs2;
приведет к синтаксической ошибке[4]. Также ошибочным будет вызов функции min(), если для данного типа не определена операция меньше. С++ не позволяет задать ограничения для типов, подставляемых в шаблоны. Как вы думаете, было бы полезным иметь такую возможность? Если да, попробуйте придумать синтаксис задания ограничений и перепишите в нем определение класса Example2. Если нет, поясните почему.
Упражнение 2.17
Как было показано в предыдущем упражнении, попытка использовать шаблон Example2 с типом, для которого не определена операция меньше, приведет к синтаксической ошибке. Однако ошибка проявится только тогда, когда в тексте компилируемой программы действительно встретится вызов функции min(), в противном случае компиляция пройдет успешно. Как вы считаете, оправдано ли такое поведение? Не лучше ли предупредить об ошибке сразу, при обработке описания шаблона? Поясните свое мнение.