Язык программирования C++. Вводный курс

Использование шаблонов


Наш класс 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<double> da(array_size);

Array<char> ca(array_size);

Что делает компилятор, встретив такое объявление? Подставляет текст шаблона Array, заменяя параметр elemType на тот тип, который указан в каждом конкретном случае. Следовательно, объявления членов приобретают в первом случае такой вид:

// Array<int> ia(array_size);

int _size;

int *_ia;

Заметим, что это в точности соответствует определению массива IntArray.

Для оставшихся двух случаев мы получим следующий код:



// Array<double> da(array_size);
int     _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>
#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 происходит как в базовом, так и в производном классах. Определение

ArrayRC<int> ia_rc(10);

ведет себя точно так же, как определение IntArrayRC из предыдущего раздела. Изменим пример использования из предыдущего раздела. Прежде всего, чтобы оператор



// функцию swap() тоже следует сделать шаблоном
swap( ia1, 1, ia1.size() );

был допустимым, нам потребуется представить функцию swap() в виде шаблона.



#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:



#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;

}

Упражнение 2.13

Пусть мы имеем следующие объявления типов:



template<class elemType> class Array;
enum Status { ... };

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);

(e) Array< Pstring > aps(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 {
public:

  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");

(f) cout << "exs: " << exs << endl;

Упражнение 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(), в противном случае компиляция пройдет успешно. Как вы считаете, оправдано ли такое поведение? Не лучше ли предупредить об ошибке сразу, при обработке описания шаблона? Поясните свое мнение.


Содержание раздела