Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

782

// правильно: рассматриваются аргументы по умолчанию из обоих объявлений template <class Type=string, int size>

class Buffer;

(Отметим, что аргументы по умолчанию для параметров шаблонов не поддерживаются в компиляторах, реализованных до принятия стандарта C++. Чтобы примеры из этой книги, в частности из главы 12, компилировались большинством современных компиляторов, мы не использовали такие аргументы.)

Внутри определения шаблона его имя можно применять как спецификатор типа всюду, где допустимо употребление имени обычного класса. Вот более полная версия

template <class Type> class QueueItem { public:

QueueItem( const Type & ); private:

Type item; QueueItem *next;

определения шаблона QueueItem:

};

Обратите внимание, что каждое появление имени QueueItem в определении шаблона

это сокращенная запись для

QueueItem<Type>

Такую сокращенную нотацию можно употреблять только внутри определения QueueItem (и, как мы покажем в следующих разделах, в определениях его членов, которые находятся вне определения шаблона класса). Если QueueItem применяется как спецификатор типа в определении какого-либо другого шаблона, то необходимо задавать полный список параметров. В следующем примере шаблон класса используется в определении шаблона функции display. Здесь за именем шаблона класса QueueItem должны идти параметры,

template <class Type>

void display( QueueItem<Type> &qi )

{

QueueItem<Type> *pqi = &qi; // ...

т.е. QueueItem<Type>.

}

16.1.1. Определения шаблонов классов Queue и QueueItem

Ниже представлено определение шаблона класса Queue. Оно помещено в заголовочный файл Queue.h вместе с определением шаблона QueueItem:

С++ для начинающих

783

#ifndef QUEUE_H #define QUEUE_H

// объявление QueueItem

template <class T> class QueueItem;

template <class Type> class Queue { public:

Queue() : front( 0 ), back ( 0 ) { } ~Queue();

Type& remove();

void add( const Type & ); bool is_empty() const { return front == 0;

}

private:

QueueItem<Type> *front; QueueItem<Type> *back;

};

#endif

При использовании имени Queue внутри определения шаблона класса Queue список параметров <Type> можно опускать. Однако пропуск списка параметров шаблона QueueItem в определении шаблона Queue недопустим. Так, объявление члена front

template <class Type> class Queue { public:

//...

private:

//ошибка: список параметров для QueueItem неизвестен

QueueItem<Type> *front;

является ошибкой:

}

Упражнение 16.1

(a) template <class Type> class Container1;

template <class Type, int size>

Найдите ошибочные объявления (или пары объявлений) шаблонов классов:

(b) template <class T, U, class V> class Container1;

class Container2;

С++ для начинающих

784

(c)template <class C1, typename C2>

(d)template <typename myT, class myT>

(e)template <class Type, int *pi> class Container3 {};

class Container4 {};

(f) template <class Type, int val = 0> class Container6;

template <class T = complex<double>, int v>

class Container5;

class Container6;

Упражнение 16.2

template <class elemenType> class ListItem;

template <class elemType> class List {

public:

List<elemType>()

: _at_front( 0 ), _at_end( 0 ), _current( 0 ), _size( 0 ) {}

List<elemType>( const List<elemType> & ); List<elemType>& operator=( const List<elemType> & );

~List();

void insert( ListItem *ptr, elemType value ); int remove( elemType value );

ListItem *find( elemType value );

void display( ostream &os = cout ); int size() { return _size; }

private:

ListItem *_at_front;

ListItem *_at_end;

ListItem *_current; int _size

Следующее определение шаблона List некорректно. Как исправить ошибку?

С++ для начинающих

785

};

16.2. Конкретизация шаблона класса

В определении шаблона указывается, как следует строить индивидуальные классы, если заданы один или более фактических типов или значений. По шаблону Queue автоматически генерируются экземпляры классов Queue с разными типами элементов. Например, если написать:

Queue<int> qi;

то из обобщенного определения шаблона автоматически создается класс Queue для объектов типа int.

Генерация конкретного класса из обобщенного определения шаблона называется конкретизацией шаблона. При такой конкретизации Queue для объектов типа int каждое вхождение параметра Type в определении шаблона заменяется на int, так что

template <class int> class Queue { public:

Queue() : front( 0 ), back ( 0 ) { } ~Queue();

int& remove();

void add( const int & ); bool is_empty() const { return front == 0;

}

private:

QueueItem<int> *front; QueueItem<int> *back;

определение класса Queue принимает вид:

};

Чтобы создать класс Queue для объектов типа string, надо написать:

Queue<string> qs;

При этом каждое вхождение Type в определении шаблона будет заменено на string. Объекты qi и qs являются объектами автоматически созданных классов.

Каждый конкретизированный по одному и тому же шаблону экземпляр класса совершенно не зависит от всех остальных. Так, у Queue для типа int нет никаких прав доступа к неоткрытым членам того же класса для типа string.

Конкретизированный экземпляр шаблона будет иметь соответственно имя Queue<int> или Queue<string>. Части <int> и <string>, следующие за именем Queue, называются фактическими аргументами шаблона. Они должны быть заключены в угловые скобки и отделяться друг от друга запятыми. В имени конкретизируемого шаблона аргументы всегда должны задаваться явно. В отличие от аргументов шаблона функции, аргументы шаблона класса никогда не выводятся из контекста:

С++ для начинающих

786

Queue qs; // ошибка: как конкретизируется шаблон?

Конкретизированный шаблон класса Queue можно использовать в программе всюду, где

//типы возвращаемого значения и обоих параметров конкретизированы из

//шаблона класса Queue

extern Queue< complex<double> >

foo( Queue< complex<double> > &, Queue< complex<double> > & );

//указатель на функцию-член класса, конкретизированного из шаблона Queue bool (Queue<double>::*pmf)() = 0;

//явное приведение 0 к указателю на экземпляр Queue

допустимо употребление типа обычного класса:

Queue<char*> *pqc = static_cast< Queue<char*>* > ( 0 );

Объекты типа класса, конкретизированного по шаблону Queue, объявляются и

extern Queue<double> eqd; Queue<int> *pqi = new Queue<int>; Queue<int> aqi[1024];

int main() { int ix;

if ( ! pqi->is_empty() )

ix = pqi->remove();

//...

for ( ix = 0; ix < 1024; ++ix ) eqd[ ix ].add( ix );

// ...

используются так же, как объекты обычных классов:

}

В объявлении и определении шаблона можно ссылаться как на сам шаблон, так и на

// объявление шаблона функции

template <class Type>

// ссылается на обобщенный шаблон

void bar( Queue<Type> &,

Queue<double> &

// ссылается на конкретизированный шаблон

конкретизированный по нему класс:

)

Однако вне такого определения употребляются только конкретизированные экземпляры. Например, в теле обычной функции всегда надо задавать фактические аргументы

void foo( Queue<int> &qi )

{

Queue<int> *pq = &qi; // ...

шаблона Queue:

С++ для начинающих

787

}

Шаблон класса конкретизируется только тогда, когда имя полученного экземпляра употребляется в контексте, где требуется определение шаблона. Не всегда определение класса должно быть известно. Например, перед объявлением указателей и ссылок на

class Matrix;

Matrix *pm; // правильно: определение класса Matrix знать необязательно

класс его знать необязательно:

void inverse( Matrix & ); // тоже правильно

Поэтому объявление указателей и ссылок на конкретизированный шаблон класса не приводит к его конкретизации. (Отметим, что в некоторых компиляторах, написанных до принятия стандарта C++, шаблон конкретизируется при первом упоминании имени конкретизированного класса в тексте программы.) Так, в функции foo() объявляются

// Queue<int> не конкретизируется при таком использовании в foo() void foo( Queue<int> &qi )

{

Queue<int> *pqi = &qi; // ...

указатель и ссылка на Queue<int>, но это не вызывает конкретизации шаблона Queue:

}

Определение класса необходимо знать, когда определяется объект этого типа. В следующем примере определение obj1 ошибочно: чтобы выделить для него память,

class Matrix;

Matrix obj1; // ошибка: класс Matrix не определен class Matrix { ... };

компилятору необходимо знать размер класса Matrix:

Matrix obj2; // правильно

Таким образом, конкретизация происходит тогда, когда определяется объект класса, конкретизированного по этому шаблону. В следующем примере определение объекта qi приводит к конкретизации шаблона Queue<int>:

Queue<int> qi; // конкретизируется Queue<int>

Определение Queue<int> становится известно компилятору именно в этой точке, которая называется точкой конкретизации данного класса.

Если имеется указатель или ссылка на конкретизированный шаблон, то конкретизация также производится в момент обращения к объекту, на который они ссылаются. В

определенной выше функции foo() класс Queue<int> конкретизируется в следующих случаях: когда разыменовывается указатель pqi, когда ссылка qi используется для

С++ для начинающих

788

получения значения именуемого объекта и когда pqi или qi употребляются для доступа к

void foo( Queue<int> &qi )

{

Queue<int> *pqi = &qi;

//Queue<int> конкретизируется в результате вызова функции-члена pqi->add( 255 );

//...

членам или функциям-членам этого класса:

}

Определение Queue<int> становится известным компилятору еще до вызова функции-

члена add() из foo().

Напомним, что в определении шаблона класса Queue есть также ссылка на шаблон

template <class Type> class Queue { public:

// ...

private:

QueueItem<Type> *front; QueueItem<Type> *back;

QueueItem:

};

При конкретизации Queue типом int члены front и back становятся указателями на QueueItem<int>. Следовательно, конкретизированный экземпляр Queue<int> ссылается на экземпляр QueueItem, конкретизированный типом int. Но поскольку соответствующие члены являются указателями, то QueueItem<int> конкретизируется лишь в момент их разыменования в функциях-членах класса Queue<int>.

Наш класс QueueItem служит вспомогательным средством для реализации класса Queue и не будет непосредственно употребляться в вызывающей программе. Поэтому пользовательская программа способна манипулировать только объектами Queue.

Конкретизация шаблона QueueItem происходит лишь в момент конкретизации шаблона класса Queue или его членов. (В следующих разделах мы рассмотрим конкретизации членов шаблона класса.)

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

определение конструктора класса QueueItem не подходит для конкретизации общего

template <class Type> class QueueItem { public:

QueueItem( Type ); // неудачное проектное решение

// ...

вида?

};

С++ для начинающих

789

В данном определении аргумент передается по значению. Это допустимо, если QueueItem конкретизируется встроенным типом (например, QueueItem<int>). Но если такая конкретизация производится для объемного типа (скажем, Matrix), то накладные расходы, вызванные неправильным выбором на этапе проектирования, становятся неприемлемыми. (В разделе 7.3 обсуждались вопросы производительности, связанные с передачей параметров по значению и по ссылке.) Поэтому аргумент конструктора объявляется как ссылка на константный тип:

QueueItem( const Type & );

Следующее определение приемлемо, если у типа, для которого конкретизируется

template <class Type> class QueueItem {

//...

public:

//потенциально неэффективно

QueueItem( const Type &t ) { item = t; next = 0;

}

QueueItem, нет ассоциированного конструктора:

};

Если аргументом шаблона является тип класса с конструктором (например, string), то item инициализируется дважды! Конструктор по умолчанию string вызывается для инициализации item перед выполнением тела конструктора QueueItem. Затем для созданного объекта item производится почленное присваивание. Избежать такого можно

спомощью явной инициализации item в списке инициализации членов внутри

template <class Type> class QueueItem {

//...

public:

//item инициализируется в списке инициализации членов конструктора

QueueItem( const Type &t )

:item(t) { next = 0; }

определения конструктора QueueItem:

};

(Списки инициализации членов и основания для их применения обсуждались в разделе

14.5.)

16.2.1. Аргументы шаблона для параметров-констант

Параметр шаблона класса может и не быть типом. На аргументы, подставляемые вместо таких параметров, накладываются некоторые ограничения. В следующем примере мы изменяем определение класса Screen (см. главу 13) на шаблон, параметризованный высотой и шириной:

С++ для начинающих

790

template <int hi, int wid> class Screen {

public:

Screen() : _height( hi ), _width( wid ), _cursor ( 0 ), _screen( hi * wid, '#' )

{ }

// ...

 

private:

_screen;

string

string::size_type _cursor;

short

_height;

short

_width;

};

 

typedef Screen<24,80> termScreen; termScreen hp2621;

Screen<8,24> ancientScreen;

Выражение, с которым связан параметр, не являющийся типом, должно быть константным, т.е. вычисляемым во время компиляции. В примере выше typedef termScreen ссылается на экземпляр шаблона Screen<24,80>, где аргумент шаблона для hi равен 24, а для wid 80. В обоих случаях аргумент это константное выражение.

Однако для шаблона BufPtr конкретизация приводит к ошибке, так как значение указателя, получающееся при вызове оператора new(), становится известно только во

template <int *ptr> class BufPtr { ... };

// ошибка: аргумент шаблона нельзя вычислить во время компиляции

время выполнения:

BufPtr< new int[24] > bp;

Не является константным выражением и значение неконстантного объекта. Его нельзя использовать в качестве аргумента для параметра-константы шаблона. Однако адрес любого объекта в области видимости пространства имен, в отличие от адреса локального объекта, является константным выражением (даже если спецификатор const отсутствует), поэтому его можно применять в качестве аргумента для параметра-

template <int size> Buf { ... }; template <int *ptr> class BufPtr { ... };

int size_val = 1024;

const int c_size_val = 1024;

Buf< 1024 > buf0; // правильно

Buf< c_size_val > buf1; // правильно

Buf< sizeof(size_val) > buf2; // правильно: sizeof(int)

BufPtr< &size_val > bp0; // правильно

// ошибка: нельзя вычислить во время компиляции

константы. Константным выражением будет и значение оператора sizeof:

Buf< size_val > buf3;

С++ для начинающих

791

Вот еще один пример, иллюстрирующий использование параметра-константы для представления константного значения в определении шаблона, а также применение его

template < class Type, int size > class FixedArray {

public:

FixedArray( Type *ar ) : count( size )

{

for ( int ix = 0; ix < size; ++ix ) array[ ix ] = ar[ ix ];

}

private:

Type array[ size ]; int count;

};

int ia[4] = { 0, 1, 2, 3 };

аргумента для задания значения этого параметра:

FixedArray< int, sizeof( is ) / sizeof( int ) > iA{ ia );

Выражения с одинаковыми значениями считаются эквивалентными аргументами для параметров-констант шаблона. Так, все три экземпляра Screen ссылаются на один и тот

const int width = 24; const int height = 80;

// все это Screen< 24, 80 > Screen< 2*12, 40*2 > scr0; Screen< 6+6+6+6, 20*2 + 40 > scr1;

же конкретизированный из шаблона класс Screen<24,80>:

Screen< width, height > scr2;

Между типом аргумента шаблона и типом параметра-константы допустимы некоторые преобразования. Их множество является подмножеством преобразований, допустимых для аргументов функции:

трансформации l-значений, включающие преобразование l-значения в r-

template <int *ptr> class BufPtr { ... };

int array[10];

значение, массива в указатель и функции в указатель:

BufPtr< array > bpObj; // преобразование массива в указатель

template <const int *ptr> class Ptr { ... }; int iObj;

преобразования квалификаторов: