Меню сайта |
|
|
Категория |
|
|
Развлечение |
|
|
ON - LINE |
|
|
Опрос |
|
|
Оbserver Ward |
Онлайн всего: 3 Гостей: 3 Пользователей: 0
|
|
Друзья сайта |
|
|
Hаша кнопка |
Для обмена банерами , наша кнопка для размещения у вас на сайте
|
|
|
16. Шаблоны классов (3)
16.10. Частичные специализации шаблонов классов A
Если у шаблона класса есть несколько параметров, то можно специализировать его только для одного или нескольких аргументов, оставляя другие неспециализированными. Иными словами, допустимо написать шаблон, соответствующий общему во всем, кроме тех параметров, вместо которых подставлены фактические типы или значения. Такой механизм носит название частичной специализации шаблона класса. Она может понадобиться при определении реализации, более подходящей для конкретного набора аргументов.
Рассмотрим шаблон класса Screen, введенный в разделе 16.2. Частичная специализации Screen<hi,80> дает более эффективную реализацию для экранов с 80 столбцами:
template <int hi, int wid> class Screen { // ... }; // частичная специализация шаблона класса Screen template <int hi> class Screen<hi, 80> { public: Screen(); // ... private: string _screen; string::size_type _cursor; short _height; // для экранов с 80 колонками используются специальные алгоритмы };
Частичная специализация шаблона класса - это шаблон, и ее определение похоже на определение шаблона. Оно начинается с ключевого слова template, за которым следует список параметров, заключенный в угловые скобки. Список параметров здесь отличается от соответствующего списка параметров общего шаблона. Для частичной специализации шаблона Screen есть только один параметр-константа hi, поскольку значение второго аргумента равно 80, т.е. в данном списке представлены только те параметры, для которых фактические аргументы еще неизвестны.
Имя частичной специализации совпадает с именем того общего шаблона, которому она соответствует, в нашем случае Screen. Однако за ее именем всегда следует список аргументов. В примере выше этот список выглядит как . Поскольку значение аргумента для первого параметра шаблона неизвестно, то на этом месте в списке стоит имя параметра шаблона; вторым же аргументом является значение 80, которым частично специализирован шаблон.
Частичная специализация шаблона класса неявно конкретизируется при использовании в программе. В следующем примере частичная специализация конкретизируется аргументом шаблона 24 вместо hi:
Screen<24,80> hp2621;
Обратите внимание, что экземпляр Screen<24,80> может быть конкретизирован не только из частично специализированного, но и из общего шаблона. Почему же тогда компилятор остановился именно на частичной специализации? Если для шаблона класса объявлены частичные специализации, компилятор выбирает то определение, которое является наиболее специализированным для заданных аргументов. Если же ни одно из них не подходит, используется общее определение шаблона. Например, при конкретизации экземпляра Screen<40,132> соответствующей аргументам шаблона специализации нет. Наш вариант применяется только для конкретизации типа Screen с 80 колонками.
Определение частичной специализации не связано с определением общего шаблона. У него может быть совершенно другой набор членов, а также собственные определения функций-членов, статических членов и вложенных типов. Содержащиеся в общем шаблоне определения членов никогда не употребляются для конкретизации членов его частичной специализации. Например, для частичной специализации Screen должен быть определен свой конструктор:
// конструктор для частичной специализации Screen<hi,80> template <int hi> Screen<hi,80> ::Screen() : _height( hi ), _cursor( 0 ), _screen( hi * 80, bk ) { }
Если для конкретизации некоторого класса применяется частичная специализация, то определение конструктора из общего шаблона не используется даже тогда, когда определение конструктора Screen отсутствует. 16.11. Разрешение имен в шаблонах классов A
При обсуждении разрешения имен в шаблонах функций (см. раздел 10.9) мы уже говорили о том, что этот процесс выполняется в два шага. Так же разрешаются имена и в определениях шаблонов классов и их членов. Каждый шаг относится к разным видам имен: первый - к тем, которые имеют один и тот же смысл во всех экземплярах шаблона, а второй - к тем, которые потенциально могут иметь разный смысл в разных экземплярах. Рассмотрим несколько примеров, где используется функция-член remove() шаблона класса Queue:
// Queue.h: #include <iostream> #include <cstdlib>
// определение класса Queue
template <class Type> Type Queue<Type>::remove() { if ( is_empty() ) { cerr << "remove() вызвана для пустой очереди\n"; exit(-1); } QueueItem<Type> *pt = front; front = front->next; Type retval = pt->item; delete pt;
cout <<"удалено значение: "; cout << retval << endl;
return retval; }
В выражении
cout << retval << endl;
переменная retval имеет тип Type, и ее фактический тип неизвестен до конкретизации функции-члена remove(). То, какой оператор operator<<() будет выбран, зависит от фактического типа retval, подставленного вместо Type. При разных конкретизациях remove() могут вызываться разные operator<<(). Поэтому мы говорим, что выбранный оператор вывода зависит от параметра шаблона.
Однако для вызова функции exit() ситуация иная. Ее фактическим аргументом является литерал, значение которого одинаково при всех конкретизациях remove(). Поскольку при обращении к функции не используются аргументы, типы которых зависят от параметра шаблона Type, гарантируется, что всегда будет вызываться exit(), объявленная в заголовочном файле cstdlib. По той же причине в выражении
cout << "удалено значение: ";
всегда вызывается глобальный оператор
ostream& operator< < ( ostream &, const char * );
Аргумент "удалено значение: " - это C-строка символов, и ее тип не зависит от параметра шаблона Type. Поэтому в любом конкретизированном экземпляре remove()употребление operator<<() имеет одинаковый смысл. Один и тот же смысл во всех конкретизациях шаблона имеют те конструкции, которые не зависят от параметров шаблона.
Таким образом, два шага разрешения имени в определениях шаблонов классов или их членов состоят в следующем:
1. Имена, не зависящие от параметров шаблона, разрешаются во время его определения. 2. Имена, зависящие от параметров шаблона, разрешаются во время его конкретизации.
Такой подход удовлетворяет требованиям как разработчика класса, так и его пользователя. Например, разработчикам необходимо управлять процессом разрешения имен. Если шаблон класса входит в состав библиотеки, в которой определены также другие шаблоны и функции, то желательно, чтобы при конкретизации шаблона класса и его членов по возможности применялись именно библиотечные компоненты. Это гарантирует первый шаг разрешения имени. Если использованное в определении шаблона имя не зависит от параметров шаблона, то оно разрешается в результате просмотра всех объявлений, видимых в заголовочном файле, включенном перед определением шаблона.
Разработчик класса должен позаботиться о том, чтобы были видимы объявления всех не зависящих от параметров шаблона имен, употребленных в его определении. Если объявление такого имени не найдено, то определение шаблона считается ошибочным. Если бы перед определением функции-члена remove() в шаблоне класса Queue не были включены файлы iostream и cstdlib, то в выражении
cout < < "удалено значение: ";
и при компиляции вызова функции exit() были бы обнаружены ошибки.
Второй шаг разрешения имени необходим, если поиск производится среди функций и операторов, зависящих от типа, которым конкретизирован шаблон. Например, если шаблон класса Queue конкретизируется типом класса LongDouble (см. раздел 16.9), то желательно, чтобы внутри функции-члена remove()в следующем выражении
cout << retval << endl;
вызывался оператор operator<< (), ассоциированный с классом LongDouble:
#include "Queue.h" #include "ldouble.h" // содержит: // class LongDouble { ... }; // ostream& operator<<( ostream &, const LongDouble & );
int main() { // конкретизация Queue<LongDouble> Queue<LongDouble> *qld = new Queue<LongDouble>;
// конкретизация Queue<LongDouble>::remove() // вызывает оператор вывода для LongDouble qld->remove(); // ... }
Место в программе, где происходит конкретизация шаблона, называется точкой конкретизации. Она определяет, какие объявления принимаются компилятором во внимание для имен, зависящих от параметров шаблона.
Точка конкретизации шаблона всегда находится в области видимости пространства имен и непосредственно предшествует объявлению или определению, которое ссылается на конкретизированный экземпляр. Точка конкретизации функции-члена или статического члена шаблона класса всегда следует непосредственно за объявлением или определением, которое ссылается на конкретизированный член.
В предыдущем примере точка конкретизации Queue находится перед main(), и при разрешении зависящих от параметров имен, которые используются в определении шаблона Queue, компилятор просматривает все объявления до этой точки. Аналогично при таком разрешении в определении remove() компилятор просматривает все объявления до точки конкретизации, расположенной после main().
Как отмечалось в разделе 16.2, шаблон конкретизируется, если он используется в контексте, требующем полного определения класса. Члены шаблона не конкретизируются автоматически вместе с ним, а лишь тогда, когда сами используются в программе. Поэтому точка конкретизации шаблона класса может не совпадать с точками конкретизации его членов, да и сами члены могут конкретизироваться в разных точках. Чтобы избежать ошибок, объявления имен, упоминаемых в определениях шаблона и его членов, рекомендуется помещать в заголовочные файлы, включая их перед первой конкретизацией шаблона класса или любого из его членов. 16.12. Пространства имен и шаблоны классов
Как и любое определение в глобальной области видимости, определение шаблона класса можно поместить внутрь пространства имен. (Пространства имен рассматривались в разделах 8.5 и 8.6.) Наш шаблон будет скрыт в данном пространстве имен; лишь в этом отличие от ситуации, когда шаблон определен в глобальной области видимости. При употреблении вне пространства имя шаблона следует либо квалифицировать его именем, либо воспользоваться using-объявлением:
#include <iostream> #include <cstdlib>
namespace cplusplus_primer { template <class Type> class Queue { // ... }; template <class Type> Type Queue<Type>::remove() { // ... } }
Если имя Queue шаблона класса используется вне пространства имен cplusplus_primer, то оно должно быть квалифицировано этим именем или введено с помощью using-объявления. Во всех остальных отношениях шаблон Queue используется так, как описано выше: конкретизируется, может иметь функции-члены, статические члены, вложенные типы и т.д. Например:
int main() { using cplusplus_primer Queue; // using-объявление
// ссылается на шаблон класса в пространстве имен cplusplus_primer Queue<int> *p_qi = new Queue <int>; // ... p_qi->remove(); }
Шаблон cplusplus_primer::Queue конкретизируется, так как использован в выражении new:
... = new Queue<int>;
p_qi - это указатель на тип класса cplusplus_primer::Queue<int>
Когда он применяется для адресации функции-члена remove(), то речь идет о члене именно этого конкретизированного экземпляра класса.
Объявление шаблона класса в пространстве имен влияет также на объявления специализаций и частичных специализаций шаблона класса и его членов (см. разделы 16.9 и 16.10). Такая специализация должна быть объявлена в том же пространстве имен, где и общий шаблон.
В следующем примере в пространстве имен cplusplus_primer объявляются специализации типа класса Queue и функции-члена remove() класса Queue:
#include <iostream> #include <cstdlib> namespace cplusplus_primer {
template <class Type> class Queue { ... };
template <class Type> Type Queue<Type>::remove() { ... }
// объявление специализации // для cplusplus_primer::Queue<char *> template<> class Queue<char*> { ... };
// объявление специализации // для функции-члена cplusplus_primer::Queue<double>::remove() template<> double Queue<double>::remove() { ... } }
Хотя специализации являются членами cplusplus_primer, их определения в этом пространстве отсутствуют. Определить специализацию шаблона можно и вне пространства имен при условии, что определение будет находиться в некотором пространстве, объемлющем cplusplus_primer, и имя специализации будет квалифицировано его именем :
namespace cplusplus_primer { // определение Queue и его функций-членов }
// объявление специализации // cplusplus_primer::Queue<char*>
template<> class cplusplus_primer::Queueprimer::Queue<char*> и функции-члена remove() для класса cplusplus_primer::Queue находятся в глобальной области видимости. Поскольку такая область содержит пространство имен cplusplus_primer, а имена специализаций квалифицированы его именем, то определения специализаций для шаблона Queue вполне законны. 16.13. Шаблон класса Array
В этом разделе мы завершим реализацию шаблона класса Array, введенного в разделе 2.5 (этот шаблон будет распространен на одиночное наследование в разделе 18.3 и на множественное наследование в разделе 18.6). Так выглядит полный заголовочный файл:
#ifndef ARRAY_H #define ARRAY_H #include iostream>
template <class elemType> class Array; template <class elemType> ostream& operator<<( ostream &, Array<elemType> & );
template <class elemType> class Array { public: explicit Array( int sz = DefaultArraySize ) { init( 0, sz ); }
Array( const elemType *ar, int sz ) { init( ar, sz ); }
Array( const Array &iA ) { init( iA._ia, iA._size ); }
~Array() { delete[] _ia; }
Array & operator=( const Array & ); int size() const { return _size; }
elemType& operator[]( int ix ) const { return _ia[ix]; }
ostream &print( ostream& os = cout ) const; void grow();
void sort( int,int ); int find( elemType ); elemType min(); elemType max(); private: void init( const elemType*, int ); void swap( int, int );
static const int DefaultArraySize = 12;
int _size; elemType *_ia; };
#endif
Код, общий для реализации всех трех конструкторов, вынесен в отдельную функцию-член init(). Поскольку она не должна напрямую вызываться пользователями шаблона класса Array, мы поместили ее в закрытую секцию:
template <class elemType> void Array<elemType>::init( const elemType *array, int sz ) { _size = sz; _ia = new elemType[ _size ];
for ( int ix = 0; ix < _size; ++ix ) if ( ! array ) _ia[ ix ] = 0; else _ia[ ix ] = array[ ix ]; }
Реализация копирующего оператора присваивания не вызывает затруднений. Как отмечалось в разделе 14.7, в код включена защита от копирования объекта в самого себя:
template <class elemType> Array<elemType>& Array<elemType>::operator=( const Array<elemType>&iA ) { if ( this != &iA ) { delete[] _ia; init( iA._ia, iA._size ); } return *this; }
Функция-член print() отвечает за вывод объекта того типа, которым конкретизирован шаблон Array. Возможно, реализация несколько сложнее, чем необходимо, зато данные аккуратно размещаются на странице. Если экземпляр конкретизированного класса Array содержит элементы 3, 5, 8, 13 и 21, то выведены они будут так:
(5) < 3, 5, 8, 13, 21 >
Оператор потокового вывода просто вызывает print(). Ниже приведена реализация обеих функций:
template <class elemType> ostream& operator<( ostream &os, Array<elemType> &ar ) { return ar.print( os ); }
template <class elemType> ostream & Array<elemType>::print( ostream &os ) const { const int lineLength = 12;
os < <"( " << _size << " )< "; for ( int ix = 0; ix < _size; ++ix ) { if ( ix % lineLength == 0 && ix ) os < < "\n\t"; os < < _ia[ ix ];
// не выводить запятую за последним элементом в строке, // а также за последним элементом массива if ( ix % lineLength != lineLength-1 && ix != _size-1 ) os < < ", "; }
os < < " >\n"; return os; }
Вывод значения элемента массива в функции print() осуществляет такая инструкция:
os < < _ia[ ix ];
Для ее правильной работы должно выполняться требование к типам, которыми конкретизируется шаблон Array: такой тип должен быть встроенным либо иметь собственный оператор вывода. В противном случае любая попытка распечатать содержимое класса Array приведет к ошибке компиляции в том месте, где используется несуществующий оператор.
Функция-член grow() увеличивает размер объекта класса Array. В нашем примере - в полтора раза:
template < class elemType> void Array< elemType>::grow() { elemType *oldia = _ia; int oldSize = _size;
_size = oldSize + oldSize/2 + 1; _ia = new elemType[_size];
int ix; for ( ix = 0; ix < oldSize; ++ix ) _ia[ix] = oldia[ix];
for ( ; ix < _size; ++ix ) _ia[ix] = elemType();
delete[] oldia; }
Функции-члены find(), min() и max() осуществляют последовательный поиск во внутреннем массиве _ia. Если бы массив был отсортирован, то, конечно, их можно было бы реализовать гораздо эффективнее.
template < class elemType> elemType Array< elemType> ::min( ) { assert( _ia != 0 ); elemType min_val = _ia[0]; for ( int ix = 1; ix < _size; ++ix ) if ( _ia[ix] < min_val ) min_val = _ia[ix];
return min_val; }
template < class elemType> elemType Array< elemType> ::max() { assert( _ia != 0 ); elemType max_val = _ia[0];
for ( int ix = 1; ix < _size; ++ix ) if ( max_val < _ia[ix] ) max_val = _ia[ix];
return max_val; }
template < class elemType> int Array< elemType> ::find( elemType val ) { for ( int ix = 0; ix < _size; ++ix ) if ( val == _ia[ix] ) return ix;
return -1; }
В шаблоне класса Array есть функция-член sort(), реализованная с помощью алгоритма быстрой сортировки. Она очень похожа на шаблон функции, представленный в разделе 10.11. Функция-член swap() - вспомогательная утилита для sort(); она не является частью открытого интерфейса шаблона и потому помещена в закрытую секцию:
template < class elemType> void Array< elemType> ::swap( int i, int j ) { elemType tmp = _ia[i]; _ia[i] = _ia[j]; _ia[j] = tmp; }
template < class elemType> void Array< elemType> ::sort( int low, int high ) { if ( low > = high ) return; int lo = low; int hi = high + 1; elemType elem = _ia[low];
for ( ;; ) { while ( _ia[++lo] < elem ) ; while ( _ia[--hi] > elem ) ; if ( lo < hi ) swap( lo,hi ); else break; }
swap( low, hi ); sort( low, hi-1 ); sort( hi+1, high ); }
То, что код реализован, разумеется, не означает, что он работоспособен. try_array() - это шаблон функции, предназначенный для тестирования реализации шаблона Array:
#include "Array.h"
template <class elemType> void try_array( Array< elemType> &iA ) { cout < <"try_array: начальные значения массива\n"; cout < < iA < <endl;
elemType find_val = iA [ iA.size()-1 ]; iA[ iA.size()-1 ] = iA.min();
int mid = iA.size()/2; iA[0] = iA.max(); iA[mid] = iA[0]; cout < < "try_array: после присваиваний\n"; cout < < iA < < endl;
Array<elemType> iA2 = iA; iA2[mid/2] = iA2[mid]; cout < < "try_array: почленная инициализация\n"; cout < <iA < < endl;
iA = iA2; cout < < "try_array: после почленного копирования\n"; cout < < iA < < endl;
iA.grow(); cout < < "try_array: после вызова grow\n"; cout < <iA < <endl; int index = iA.find( find_val ); cout < <"искомое значение: " < < find_val; cout < <"\tвозвращенный индекс: " < < index < < endl;
elemType value = iA[index]; cout < < "значение элемента с этим индексом: "; cout < < value < < endl; }
Рассмотрим шаблон функции try_array(). На первом шаге печатается исходный объект Array, что подтверждает успешную конкретизацию оператора вывода шаблона, а заодно дает начальную картину, с которой можно будет сверяться при последующих модификациях. В переменной find_val хранится значение, которое мы впоследствии передадим find(). Если бы try_array() была обычной функцией, роль такого значения сыграла бы константа. Но поскольку никакая константа не может обслужить все типы, которыми допустимо конкретизировать шаблон, то приходится выбирать другой путь. Далее одним элементам Array случайным образом присваиваются значения других элементов, чтобы протестировать min(), max(), size() и, конечно, оператор взятия индекса.
Затем объект iA2 почленно инициализируется объектом iA, что приводит к вызову копирующего конструктора. После этого тестируется оператор взятия индекса с объектом ia2: производится присваивание элементу с индексом mid/2. (Эти две строки представляют интерес в случае, когда iA - производный подтип Array, а оператор взятия индекса объявлен виртуальной функцией. Мы вернемся к этому в главе 18 при обсуждении наследования.) Далее в iA почленно копируется модифицированный объект iA2, что приводит к вызову копирующего оператора присваивания класса Array. Затем проверяются функции-члены grow() и find(). Напомним, что find() возвращает значение -1, если искомый элемент не найден. Попытка выбрать из "массива" Array элемент с индексом -1 приведет к выходу за левую границу. (В главе 18 для перехвата этой ошибки мы построим производный от Array класс, который будет проверять выход за границы массива.)
Убедиться, что наша реализация шаблона работает для различных типов данных, например целых чисел, чисел с плавающей точкой и строк, поможет программа main(), которая вызывает try_array() с каждым из указанных типов:
#include "Array.C" #include "try_array.C" #include <string>
int main() { static int ia[] = { 12,7,14,9,128,17,6,3,27,5 }; static double da[] = { 12.3,7.9,14.6,9.8,128.0 }; static string sa[] = { "Eeyore", "Pooh", "Tigger", "Piglet", "Owl", "Gopher", "Heffalump" }; Array< int> iA( ia, sizeof(ia)/sizeof(int) ); Array< double> dA( da, sizeof(da)/sizeof(double) ); Array< string> sA( sa, sizeof(sa)/sizeof(string) );
cout < < "template Array<int> class\n" < < endl; try_array(iA);
cout < < "template Array<double> class\n" < < endl; try_array(dA);
cout < < "template Array<string> class\n" < < endl; try_array(sA);
return 0; }
Вот что программа выводит при конкретизации шаблона Array типом double:
try_array: начальные значения массива ( 5 )< 12.3, 7.9, 14.6, 9.8, 128 >
try_array: после присваиваний ( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 >
try_array: почленная инициализация ( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 >
try_array: после почленного копирования ( 5 )< 14.6, 14.6, 14.6, 9.8, 7.9 >
try_array: после вызова grow ( 8 )< 14.6, 14.6, 14.6, 9.8, 7.9, 0, 0, 0 > искомое значение: 128 возвращенный индекс: -1 значение элемента с этим индексом: 3.35965e-322
Выход индекса за границу массива приводит к тому, что последнее напечатанное программой значение неверно. Конкретизация шаблона Array типом string заканчивается крахом программы:
template Array<string> class
try_array: начальные значения массива ( 7 )< Eeyore, Pooh, Tigger, Piglet, Owl, Gopher, Heffalump >
try_array: после присваиваний ( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore>
try_array: почленная инициализация ( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore >
try_array: после почленного копирования ( 7 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: после вызова grow ( 11 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore, <пусто> , <пусто> , <пусто> , <пусто> >
искомое значение: Heffalump возвращенный индекс: -1 Memory fault (coredump)
Упражнение 16.11
Измените шаблон класса Array, убрав из него функции-члены sort(), find(), max(), min() и swap(), и модифицируйте шаблон try_array() так, чтобы она вместо них пользовалась обобщенными алгоритмами
|
Категория: С++ | Добавил: r2d2 (29.09.2011)
|
Просмотров: 1123
| Рейтинг: 0.0/0 |
Добавлять комментарии могут только зарегистрированные пользователи. [ Регистрация | Вход ]
|
|
Born in Ussr |
|
|
Залогиниться |
|
|
Турниры |
|
|
|