Сообщений 19 Оценка 101 [+0/-4] Оценить |
Обычные классы мы разделяли на интерфейс и реализацию, помещая интерфейс в модуль с расширением h. Этот модуль потом и подключался к другим файлам проекта оператором #include, а реализация класса просто включалась в проект.
Попробуем аналогичным образом поступить и с классами-шаблонами. Разделим наш шаблон-стек на две части: определение (листинг 1) и реализацию (листинг 2).
Листинг 1. "Определение" шаблона стека//----------------------------файл TSTACK.h #ifndef TSTACK #define TSTACK #include <exception> template <class T> class TStack { struct Elem { T data; Elem *next; Elem (const T& d, Elem *p) :data(d), next(p) { } }; Elem * Head; int count; TStack(const TStack &); // закрыли копирование TStack& operator=(const TStack &); // закрыли присваивание public: class Error: public exception { }; TStack(); ~TStack(); void push(const T& d); T top() const ; T pop(); bool empty() const; int Count(); }; #endif |
// "подключение" определения #include "Tstack.h" template <class T> TStack<T>::TStack(): Head(0), count(0) {} template <class T> TStack<T>::~TStack() { while(!empty()) { T t = pop(); } } template <class T> void TStack<T>::push(const T& d) { Head = new Elem(d, Head); ++count; } template <class T> T TStack<T>::top() const { if(!empty()) return Head->data; elsethrow Error(); } template <class T> T TStack<T>::pop() { if (empty()) throw Error(); T top = Head->data; Elem *oldHead = Head; Head = Head->next; delete oldHead; --count; return top; } template <class T> bool TStack<T>::empty() const { return Head==0; } template <class T> int TStack<T>::Count() { return count; } |
Как видите, чисто формально это ничем не отличается от того, что обычно делается с простыми классами. Третьим модулем в проект включается функция main(), в которой мы наш шаблон-стек попытаемся использовать (листинг 3).
Листинг 3. "Использование" шаблона-стека#include "Tstack.h" #include <iostream> #include <string> usingnamespace std; int main() { TStack<double> t; // кладем в стек числа t.push(1); t.push(2); t.push(3); cout << t.Count() << endl; // пока стек не пустойwhile (!t.empty()) { cout << t.top() << endl; // выводим число с вершиныdouble p = t.pop(); // удаляем элемент из стека } cout << t.Count() << endl; TStack<string> S; S.push("one"); // кладем в стек строки S.push("two"); S.push("three"); cout << S.Count() << endl; // пока стек не пустойwhile (!S.empty()) { string p = S.pop(); // удаляем элемент из стека cout << p << endl; // выводим строку } cout << S.Count() << endl; try { string p = S.pop(); } // стек пустой - генерируется исключениеcatch(const exception &e) { cout << e.what() << endl; } return 0; } |
Транслируется все нормально, без ошибок. Однако линкер Visual C++ 7.x выдает 13 сообщений!!! Причина заключается в отсутствии инстанцирования шаблона.
Позвольте, но вот же в функции main()!… Однако, у нас три отдельных модуля в проекте. Поэтому когда компилятор обрабатывает определение и реализацию шаблона-стека, у него нет указаний, что шаблон должен быть воплощен (instantiated) для каких-то конкретных аргументов. Когда же компилятор "видит" определение объекта-стека,
TStack<double> t;
|
или
TStack<string> S; |
в его поле зрения нет ни определения, ни реализации. Поэтому он не может произвести воплощение шаблона, но предполагает, что где-то в других частях это сделано, и оставляет "пустую" ссылку линкеру. Линкер, естественно, ничего не находит, так как воплощения-то компилятор не сделал!
Аналогичная картина — с шаблонами функций. Пусть у нас есть шаблон функции суммирования массива (листинг 4).
Листинг 4. Шаблон функции суммирования последовательности элементов массива//----------------------------файл Summa.h #ifndef SUMMA #define SUMMA template <typename T> T Summa(T const *begin, T const *end) { T total = RT(); // инициализация нулемwhile (begin != end) { total += *begin; ++begin; } return total; } #endif |
И мы подключаем его к модулю, содержащему функцию main() с помощью директивы #include. Попробуем вместо подключения:
#include "Summa.h" |
прописать в main() просто прототип-объявление (листинг 5, выделенная строка).
Листинг 5. "Использование" шаблона функции.#include <iostream> #include <string> usingnamespace std; template <typename T> // прототип шаблона функцииT Summa(T const *begin, T const *end); int main() { int a[10]= {1,2,3,4,5,6,7,8,9,10}; cout << Summa(a, a+10) << endl; double b[10]= {1.1,2,3,4,5,6,7,8,9,10.1}; cout << Summa(b, b+10) << endl; string d[4] = {"1+","2+","3+","4+"}; cout << Summa(d, d+4) << endl; short s[2]= { 10000,10000 }; cout << Summa(s, s+2) << endl; int x; cin >> x; return 0; } |
Транслируется все без ошибок, однако линкер не находит определения функции и не может скомпоновать программу.
Таким образом, большинство современных компиляторов C++ не позволяет "делить" шаблоны на части и транслировать отдельно. Шаблон представляет собой только заготовку для построения кода: пока шаблон не воплощен, объектного кода из него не получится, и линкеру нечего будет собирать. Модули, использующие шаблон, должны подключить файл с полным определением шаблона с помощью #include. Мы именно так и поступали до сих пор. Такой способ организации кода с шаблонами называется "модель включения" [1]. Его главный недостаток нам уже хорошо известен — транслируется вся большая программа сразу.
Стандарт С++ обеспечивает еще один вариант организации кодов с шаблонами — модель явного воплощения. При такой организации шаблоны воплощаются вручную (то есть самим программистом). Рассмотрим пример с шаблоном функции Summa(). Создадим в проекте 3 модуля:
//--------------------------------файл-SummaInst.h #include <string> usingnamespace std; #include"Summa.h"// подключение шаблона // набор директив явного воплощенияtemplatedouble Summa<double>(doubleconst *begin, doubleconst *end); templateint Summa<int> (intconst *begin, intconst *end); templateshort Summa<short> (shortconst *begin, shortconst *end); template string Summa<string>(string const *begin, string const *end); |
Объявление явного воплощения должно начинаться с ключевого слова template, за которым следует объявление объекта, экземпляр которого надо сгенерировать. Вместо параметров шаблона должны быть подставлены конкретные типы. В нашем примере в функции main()четыре вызова функции Summa(). Поэтому в этом файле должно быть четыре объявления с аргументами как раз тех типов, которые используются во время вызовов функции Summa().
Обратите внимание, что в модуле с функцией main() мы не подключаем ни шаблон, ни файл с объявлениями явного воплощения. В этом модуле необходим только прототип шаблона. Тем не менее, все транслируется, собирается и выполняется правильно.
Можно сочетать модель явного воплощения и модель включения. Например, можно подключить файл явного воплощения SummaInst.h в модуле main.cpp
#include "SummaInst.h" |
Естественно, прототип шаблона в этом случае можно убрать.
Теперь рассмотрим модель явного воплощения для классов. Включаем в проект следующие модули:
#include "TStackDef.h" #include <string> using std::string; templateclass TStack<double>; templateclass TStack<string>; |
ПРИМЕЧАНИЕ ПРИМЕЧАНИЕ В Visual Studio 2003 объявления явного воплощения можно писать без слова class, например template TStack<double>; — система не выдает ошибок трансляции. |
Обратите внимание на директивы #include:
Таким образом, явное воплощение обеспечивает как раз такую организацию файлов с шаблонами, которую мы хотели иметь с самого начала. Однако этот способ тоже не свободен от недостатков. Как пишут Джосаттис и Вандевурд [1], программист должен тщательно следить за тем, какой тип шаблона воплощается. Для больших проектов это быстро становится слишком обременительным, поэтому они не советуют применять этот метод в промышленных проектах.
ПРИМЕЧАНИЕ Стандарт определяет еще механизм экспорта шаблонов, который обеспечивает организацию файлов с шаблонами, известную как модель разделения. Однако в настоящее время практически ни один популярный компилятор (в том числе и Visual C++.NET 2003) не поддерживает эту модель, поэтому останавливаться мы на ней не будем. Модель разделения описана в книге [1]. |
Сообщений 19 Оценка 101 [+0/-4] Оценить |