Оценка 101
[+0/-4]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
Обычные классы мы разделяли на интерфейс и реализацию, помещая интерфейс в модуль с расширением h. Этот модуль потом и подключался к другим файлам проекта оператором #include, а реализация класса просто включалась в проект.
Попробуем аналогичным образом поступить и с классами-шаблонами. Разделим наш шаблон-стек на две части: определение (листинг 1) и реализацию (листинг 2).
//----------------------------файл 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).
#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).
//----------------------------файл 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, выделенная строка).
#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]. |
Оценка 101
[+0/-4]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|