// шаблона создается? Как и любая другая функция-член, экземпляр статической функции-члена создается только при его использовании в программе. Упражнения раздела 16.1.2
Упражнение 16.9. Что такое шаблон функции? Что такое шаблон класса? Упражнение 16.10. Что происходит при создании экземпляра шаблона класса? Упражнение 16.11. Следующее определение шаблона List неправильно. Как его исправить? template <typename elemType> class ListItem; template <typename elemType> class List { public: List<elemType>(); List<elemType>(const List<elemType> &); List<elemType>& operator=(const List<elemType> &); List(); void insert(ListItem *ptr, elemType value); private: ListItem *front, *end; }; Упражнение 16.12. Напишите собственные версии шаблонов Blob и BlobPtr, включая все константные члены, которые не были представлены в тексте. Упражнение 16.13. Объясните, какой вид дружественных отношений вы выбрали бы для операторов равенства и сравнения шаблона BlobPtr. Упражнение 16.14. Напишите шаблон класса Screen, который использует параметры значения для определения высоты и ширины экрана. Упражнение 16.15. Реализуйте операторы ввода и вывода для своего шаблона Screen. Какие друзья необходимы классу Screen (если таковые вообще имеются) для работы операторов ввода и вывода? Объясните, зачем нужно каждое объявление дружественным (если таковые вообще имеются). Упражнение 16.16. Перепишите класс StrVec (см. раздел 13.5), как шаблон Vec. 16.1.3. Параметры шаблона Page 838/1103 Подобно именам параметров функций, имена параметров шаблона не имеют никакого значения. Обычно параметрам типа присваивают имя Т, но можно использовать любое другое: template <typename Foo> Foo calc(const Foo& a, const Foo& b) { Foo tmp = a; // тип tmp совпадает с типом параметров и возвращаемого // значения // ... return tmp; // типы возвращаемого значения и параметров совпадают } Параметры шаблона и область видимости Параметры шаблона следуют обычным правилам области видимости. Имя параметра шаблона применимо сразу после его объявления и до конца объявления или определения шаблона. Подобно любым другим именам, параметр шаблона скрывает любые объявления имен во внешней области видимости. Однако, в отличие от большинства других контекстов, имя, используемое как параметр шаблона, не может быть повторно использовано в пределах шаблона: typedef double А; template <typename A, typename В> void f(А а, В b) { A tmp = а; // tmp имеет тип параметра шаблона А, а не double double В; // ошибка: повторное объявление параметра шаблона В } Согласно обычным правилам сокрытия имен, определение typedef типа А скрывается определением параметра типа по имени А. Таким образом, переменная tmp не будет иметь тип double; она будет иметь любой тип, который будет передан параметру шаблона А при использовании шаблона. Поскольку нельзя многократно использовать имена параметров шаблона, объявление переменной по имени B ошибочно. Поскольку имя параметра не может быть использовано многократно, в каждом списке параметров шаблона имя параметра шаблона может присутствовать только однажды: // ошибка: повторение имени V в параметрах шаблона недопустимо template <typename V, typename V> // ... Объявления шаблона Page 839/1103
Объявление шаблона должно включить параметры шаблона: // объявляет, но не определяет compare и Blob template <typename Т> int compare(const T&, const T&); template <typename T> class Blob; Подобно параметрам функций, имена параметров шаблона не должны совпадать с таковыми в объявлениях и определениях того же шаблона: // все три случая использования calc // относятся к тому же шаблону функции template <typename Т> Т calc(const Т&, const Т&); // объявление template <typename U> U calc(const U&, const U&); // объявление // определение шаблона template <typename Type> Type calc(const Type& a, const Type& b) { /* ... */ } Конечно, у каждого объявления и определения шаблона должно быть то же количество и вид (т.е. тип или значение) параметров. По причинам, рассматриваемым в разделе 16.3, объявления всех шаблонов, необходимых данному файлу, обычно располагаются вместе в начале файла перед любым использующим их кодом. Использование членов типа Помните, как в разделах 7.4 и 7.6 использовался оператор области видимости (::) для обращения к статическим членам и членам типа. В обычном коде (не шаблона) у компилятора есть доступ к определению класса. В результате он знает, является ли имя, к которому обращаются через оператор области видимости, типом или статическим членом. Например, в коде string::size_type, компилятор имеет определение класса string и может узнать, что size_type — это тип. С учетом того, что Т является параметром типа шаблона, когда компилятор встретит такой код, как T::mem, он не будет знать до времени создания экземпляра, является ли mem типом или статической переменной-членом. Но чтобы обработать шаблон, компилятор должен знать, представляет ли имя тип. Например, если T является именем параметра типа, то как компилятор воспримет следующий код: T::size_type * p; Он должен знать, определяется ли переменная по имени p или происходит умножение Page 840/1103 статической переменной-члена по имени size_type на переменную по имени p. По умолчанию язык подразумевает, что имя, к которому обращаются через оператор области видимости, не является типом. В результате, если необходимо использовать тип-член параметра типа шаблона, следует явно указать компилятору, что имя является типом. Для этого используется ключевое слово typename: template <typename Т> typename Т::value_type top(const T& с) { if (!c.empty()) return c.back(); else return typename T::value_type(); } Функция top() ожидает контейнер в качестве аргумента, она использует ключевое слово typename для определения своего типа возвращаемого значения и создает инициализированный по умолчанию элемент (см. раздел 7.5.3), чтобы возвратить его, если у контейнера с нет никаких элементов. Когда необходимо уведомить компилятор о том, что имя представляет тип, следует использовать ключевое слово typename, а не class. Аргументы шаблона по умолчанию Аналогично тому, как можно предоставить аргументы по умолчанию для параметров функции (см. раздел 6.5.1), можно предоставить аргументы шаблона по умолчанию (default template argument). По новому стандарту можно предоставлять аргументы по умолчанию и для шаблонов функций, и для шаблонов классов. Прежние версии языка допускали аргументы по умолчанию только для шаблонов класса. В качестве примера перепишем функцию сравнения, использующую по умолчанию библиотечный шаблонный объект функции less (см. раздел 14.8.2): // compare() имеет аргумент шаблона по умолчанию, less<T> // и заданный по умолчанию аргумент функции, F() template <typename Т, typename F = less<T>> int compare(const T &v1, const T &v2, F f = F()) { if (f(v1, v2)) return -1; if (f(v2, v1)) return 1; return 0; } Page 841/1103 Здесь в шаблон добавлен второй параметр типа, F, представляющий тип вызываемого объекта (см. раздел 10.3.2), и определен новый параметр функции, f, который будет связан с вызываемым объектом. Предоставлено также значение по умолчанию для этого параметра шаблона и соответствующего ему параметра функции. Аргумент шаблона по умолчанию определяет, что функция compare() будет использовать библиотечный класс less объекта функции, экземпляр которого создается с тем же параметром типа, что и функция compare(). Заданный по умолчанию аргумент функции указывает, что параметр f будет инициализирован по умолчанию объектом типа F. Когда пользователи вызывают эту версию функции compare(), они могут предоставить собственный оператор сравнения, но не обязаны делать это: bool i = compare(0, 42); // использует less; i равно -1 // результат зависит от isbn в item1 и item2 Sales_data item1(cin), item2(cin); bool j = compare(item1, item2, compareIsbn); Первый вызов использует заданный по умолчанию аргумент функции, которым является объект типа less<T>. В этом вызове Т имеет тип int, поэтому у объекта будет тип less<int>. Этот экземпляр функции compare() будет использовать для сравнения тип less<int>. Во втором вызове передается функция compareIsbn() (см. раздел 11.2.2) и два объекта типа Sales_data. Когда функция compare() вызывается с тремя аргументами, типом третьего аргумента должен быть вызываемый объект, возвращающий тип, приводимый к типу bool и получающий аргументы типа, совместимого с типами первых двух аргументов. Как обычно, типы параметров шаблона выводятся из соответствующих им аргументов функции. В этом вызове тип T выводится как тип Sales_data, а тип F — как тип compareIsbn(). Как и с аргументами функций по умолчанию, у параметра шаблона может быть аргумент по умолчанию, только если у всех параметров справа от него также есть аргументы по умолчанию. Аргументы по умолчанию шаблона и шаблоны класса Всякий раз, когда используется шаблон класса, за именем шаблона всегда должны следовать угловые скобки. Скобки означают, что класс будет создан как экземпляр шаблона. В частности, если шаблон класса предоставляет аргументы по умолчанию для всех своих параметров и следует использовать именно их, то после имени шаблона следует поместить пустую пару угловых скобок: template <class Т = int> class Numbers { // по умолчанию Т - это int public: Numbers(Т v = 0): val(v) { } // различные операции с числами Page 842/1103 private: Т val; }; Numbers<long double> lots_of_precision; Numbers<> average_precision; // пустые <> означают тип по умолчанию Здесь создаются два экземпляра шаблона Numbers: версия average_ precision — экземпляр Numbers с заменой параметра Т типом int; версия lots_of_precision — экземпляр Numbers с заменой параметра Т типом long double. Упражнения раздела 16.1.3 Упражнение 16.17. Каковы (если есть) различия между параметром типа, объявленным с ключевым словом typename и ключевым словом class? Когда должно использоваться ключевое слово typename? Упражнение 16.18. Объясните каждое из следующих объявлений шаблона функции и укажите, допустимы ли они. Исправьте все найденные ошибки. (a) template <typename Т, U, typename V> void f1(T, U, V); (b) template <typename T> T f2(int &T); (c) inline template <typename T> T foo(T, unsigned int*); (d) template <typename T> f4(T, T); (e) typedef char Ctype; template <typename Ctype> Ctype f5(Ctype a); Упражнение 16.19. Напишите функцию, получающую ссылку на контейнер и выводящую его элементы. Используйте переменную size_type и функцию-член size() контейнера для контроля цикла, вывода элементов. Упражнение 16.20. Перепишите функцию из предыдущего упражнения так, чтобы использовать для контроля цикла итераторы, возвращаемые функциями begin() и end(). 16.1.4. Шаблоны-члены У класса (обычного или шаблона класса) может быть функция-член, которая сама является шаблоном. Такие члены называются шаблонами-членами (member template). Шаблоны-члены не могут быть виртуальными.Шаблоны-члены обычных (не шаблонных) классов В качестве примера обычного класса, у которого есть шаблон-член, определим класс, подобный стандартному типу функции удаления (deleter), используемой указателем unique_ptr (см. раздел 12.1.5). Как и у стандартной функции удаления, у данного класса будет перегруженный оператор вызова функции (см. раздел 14.8), который, получив указатель, Page 843/1103 выполняет для него оператор delete. В отличие от стандартной функции удаления, новый класс будет также выводить сообщения при каждом запуске. Поскольку создаваемую функцию удаления предстоит использовать с любым типом, сделаем оператор вызова шаблоном: // класс объекта функции, вызывающий оператор delete для указателя class DebugDelete { public: DebugDelete(std::ostream &s = std::cerr): os(s) { } // подобно любым шаблонам функции, тип Т выводится компилятором template <typename Т> void operator()(Т *p) const { os << "deleting unique_ptr" << std::endl; delete p; } private: std::ostream &os; }; Как и любой другой шаблон, шаблон-член начинается с собственного списка параметров шаблона. У каждого объекта класса DebugDelete есть переменная-член типа ostream для вывода и функция-член, которая сама является шаблоном. Этот класс можно использовать вместо оператора delete: double* p = new double; DebugDelete d; // объект, способный действовать как оператор delete d(p); // вызывает DebugDelete::operator()(double*), удаляющий p int* ip = new int; // вызывает operator()(int*) для временного объекта DebugDelete DebugDelete()(ip); Поскольку вызов объекта DebugDelete удаляет переданный ему указатель, его можно также использовать как функцию удаления для указателя unique_ptr. Чтобы переопределить функцию удаления указателя unique_ptr, укажем тип функции удаления в скобках и предоставим объект типа функции удаления конструктору (см. раздел 12.1.5): // удалить объект, на который указывает p Page 844/1103
// создает экземпляр DebugDelete::operator()<int>(int *) unique_ptr<int, DebugDelete> p(new int, DebugDelete()); // удаляет объект, на который указывает sp // создает экземпляр DebugDelete::operator()<string>(string*) unique_ptr<string, DebugDelete> sp(new string, DebugDelete()); Здесь указано, что у функции удаления p будет тип DebugDelete и что предоставлен безымянный объект этого типа в конструкторе p(). Деструктор класса unique_ptr вызывает оператор вызова типа DebugDelete. Таким образом, при каждом вызове деструктора класса unique_ptr создается также экземпляр оператора вызова класса DebugDelete. Таким образом, определения выше создадут следующие экземпляры: // примеры создания экземпляров шаблонов-членов DebugDelete void DebugDelete::operator()(int *p) const { delete p; } void DebugDelete::operator()(string *p) const { delete p; } Шаблоны-члены шаблонов класса Шаблон-член можно также определить и для шаблона класса. В данном случае у и класса, и у его члена будут собственные, независимые параметры шаблона. В качестве примера снабдим класс Blob конструктором, который получает два итератора, обозначающих диапазон копируемых элементов. Поскольку желательно обеспечить поддержку итераторов в различных видах последовательностей, сделаем этот конструктор шаблоном: template <typename Т> class Blob { template <typename It> Blob(It b, It e); // ... }; У этого конструктора есть свой собственный параметр типа шаблона, It, который он использует для типа двух параметров функции. В отличие от обычных функций-членов шаблонов класса, шаблоны-члены являются шаблонами функций. При определении шаблона-члена вне тела шаблона класса следует предоставить список параметров шаблона для шаблона класса и для шаблона функции. Список параметров для шаблона класса располагается сначала, затем следует список параметров шаблона-члена: Page 845/1103 template <typename Т> // параметр типа для класса template <typename It> // параметр типа для конструктора Blob<T>::Blob(It b, It е) : data(std::make_shared<std::vector<T>>(b, e)) { } Здесь определяется член шаблона класса, у которого есть один параметр типа шаблона Т. Сам член является шаблоном функции, имеющий параметр типа It. Создание экземпляров и шаблоны-члены Чтобы создать экземпляр шаблона-члена шаблона класса, следует предоставить аргументы для параметров шаблона и класса, и функции. Как обычно, аргументы для параметров шаблона класса определяются типом объекта, через который происходит вызов шаблона-члена. Так же как обычно, компилятор, как правило, выводит тип аргументов шаблона для собственных параметров шаблона-члена из аргументов, переданных при вызове (см. раздел 16.1.1): int ia[] = {0,1,2,3,4,5,6,7,8,9}; vector<long> vi = {0,1,2,3,4,5,6,7,8,9}; list<const char*> w = {"now", "is", "the", "time"}; // создает экземпляр класса Blob<int> // и конструктор Blob<int> с двумя параметрами типа int* Blob<int> a1(begin(ia), end(ia)); // создает экземпляр конструктора Blob<int> с двумя параметрами // типа vector<long>::iterator Blob<int> а2(vi.begin(), vi.end()); // создает экземпляр класса Blob<string> и конструктор Blob<string> // с двумя параметрами типа list<const char*>::iterator Blob<string> a3(w.begin(), w.end()); Page 846/1103
При определении a1 указывается явно, что компилятор должен создать экземпляр шаблона Blob с параметром типа int. Параметр типа для его собственных параметров конструктора будет выведен из типа результатов вызова функций begin(ia) и end(ia). Этим типом является int*. Таким образом, определение a1 создает следующий экземпляр: Blob<int>::Blob(int*, int*); Определение а2 использует уже готовый экземпляр класса Blob<int> и создает экземпляр конструктора с параметром типа It, замененным на vector<short>::iterator. Определение a3 (явно) создает экземпляр шаблона Blob с собственным параметром шаблона типа string и (неявно) экземпляр конструктора шаблона-члена этого класса с собственным параметром типа list<const char*>. Упражнения раздела 16.1.4 Упражнение 16.21. Напишите собственную версию типа DebugDelete. Упражнение 16.22. Пересмотрите программы TextQuery из раздела 12.3 так, чтобы указатель-член shared_ptr использовал тип DebugDelete как свою функцию удаления (см. раздел 12.1.4). Упражнение 16.23. Предскажите, когда будет выполняться оператор вызова в вашей основной программе запроса. Если предсказание неправильно, убедитесь, что понимаете почему. Упражнение 16.24. Добавьте в свой шаблон Blob конструктор, получающий два итератора. 16.1.5. Контроль создания экземпляра Тот факт, что экземпляр шаблона создается только при его использовании (см. раздел 16.1.1), означает, что создание того же экземпляра может происходить в нескольких объектных файлах. Когда два или более отдельно откомпилированных файла исходного кода используют тот же шаблон с теми же аргументами шаблона, создание экземпляра этого шаблона осуществляется в каждом из этих файлов. В больших системах дополнительные затраты на создание экземпляра того же шаблона в нескольких файлах могут оказаться существенными. По новому стандарту можно избежать этих дополнительных затрат за счет явного создания экземпляра (explicit instantiation). Его форма такова: extern template объявление; // объявление создания экземпляра template объявление; // определение создания экземпляра где объявление — это объявление класса или функции, в котором все параметры шаблона заменены аргументами шаблона. Например: // Page 847/1103 объявление и определение создания экземпляра extern template class Blob<string>; // объявление template int compare(const int&, const int&); // определение Когда компилятор встретит внешнее (extern) объявление шаблона, он не будет создавать код его экземпляра в этом файле. Объявление экземпляра как extern является обещанием того, что будет и не внешнее создание экземпляра в другом месте программы. Вполне может быть несколько внешних объявлений для каждого экземпляра, однако по крайней мере одно определение экземпляра должно быть. Поскольку компилятор автоматически создает экземпляр шаблона при его использовании, объявление extern должно располагаться перед любым кодом, который использует этот экземпляр: // Application.cc // экземпляры этих шаблонов должны быть созданы // в другом месте программы extern template class Blob<string>; extern template int compare(const int&, const int&); Blob<string> sa1, sa2; // экземпляр создается в другом месте // экземпляры Blob<int> и его конструктор initializer_list создаются // в этом файле Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9}; Blob<int> a2(a1); // экземпляр конструктора копий // создается в этом файле int i = compare(a1[0], а2[0]); // Page 848/1103
экземпляр создается в другом месте Файл Application.o будет создавать экземпляр класса Blob<int> наряду с его конструктором initializer_list и конструктором копий. Экземпляры функции compare<int> и класса Blob<string> не будут созданы в этом файле. Определения этих шаблонов должны быть в каком-то другом файле программы: // templateBuild.cc // файл создания экземпляра должен предоставить обычное определение для // каждого типа и функции, которые другие файлы объявляют внешними template int compare(const int&, const int&); template class Blob<string>; // создает экземпляры всех членов // шаблона класса В отличие от объявления, когда компилятор видит определение экземпляра, он создает код. Таким образом, файл templateBuild.o будет содержать определения функции compare() для экземпляра типа int и класса Blob<string>. При построении приложения следует скомпоновать файл templateBuild.o с файлом Application.o. Для каждого объявления экземпляра где-нибудь в программе должно быть определение явного создания экземпляра. Определения экземпляров создают экземпляры всех членов Определение экземпляра для шаблона класса создает экземпляры всех членов этого шаблона, включая встраиваемые функции-члены. Когда компилятор видит определение экземпляра, он не может знать, какие функции-члены использует программа. Следовательно, в отличие от обычного способа создания экземпляра шаблона класса, компилятор создает экземпляры всех членов этого класса. Даже если член класса не будет использоваться, его экземпляр будет создан все равно. Следовательно, явное создание экземпляра можно использовать только для таких типов, которые применимы со всеми членами данного шаблона. Определение экземпляра используется только для таких типов, которые применимы со всеми функциями-членами шаблона класса. Упражнения раздела 16.1.5 Упражнение 16.25. Объясните значение этих объявлений: extern template class vector<string>; template class vector<Sales_data>; Упражнение 16.26. Предположим, что класс NoDefault не имеет стандартного конструктора. |