Сообщений 19    Оценка 101 [+0/-4]         Оценить  
Система Orphus

Шаблоны и модули

Автор: Валерий Лаптев
The RSDN Group

Источник: RSDN Magazine #4-2004
Опубликовано: 30.10.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Шаблоны и модули
Явное воплощение
Литература

Шаблоны и модули

Обычные классы мы разделяли на интерфейс и реализацию, помещая интерфейс в модуль с расширением 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
Листинг 2. "Реализация" шаблона стека
      // "подключение" определения
      
#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 модуля:

Листинг 6. Файл с директивами явного воплощения.
      //--------------------------------файл-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"
    

Естественно, прототип шаблона в этом случае можно убрать.

Теперь рассмотрим модель явного воплощения для классов. Включаем в проект следующие модули:

Листинг 7. Модуль явного инстанцирования
      #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].

Литература

  1. Вандевурд Д., Джосаттис Н. Шаблоны С++: справочник разработчика / Пер.с англ. — М.: Издательский дом "Вильямс", 2003. — 544с.: ил.

Эта статья опубликована в журнале RSDN Magazine #4-2004. Информацию о журнале можно найти здесь
    Сообщений 19    Оценка 101 [+0/-4]         Оценить