Урок №175. Шаблоны классов

  Юрий Ворон  | 

    | 

  Обновлено 9 Ноя 2018  | 

 248

Из предыдущих уроков мы узнали, как с помощью шаблонов функций сделать одну версию функции, которая будет работать с разными типами данных. Хотя это значительный шаг на пути к обобщенному программированию, это не решает всех наших проблем. Рассмотрим пример такой проблемы и то, как шаблоны могут нам помочь в её решении.

Шаблоны и контейнерные классы

Из урока о контейнерных классах мы узнали, как, используя композицию, реализовать классы, содержащие несколько объектов определенного типа данных. В качестве примера мы использовали класс ArrayInt:

Хотя этот класс обеспечивает простой способ создания массива целочисленных значений, что, если нам нужно будет работать со значениями типа double? Используя традиционные методы программирования, мы бы создали новый класс ArrayDouble для работы со значениями типа double:

Хотя кода много, но классы почти идентичны, изменяется только тип данных! Как вы уже могли бы догадаться, это идеальный случай для использования шаблонов. Создание шаблона класса аналогично созданию шаблона функции. Например, создадим шаблон класса Array:

Array.h:

Как вы можете видеть, эта версия почти идентична версии ArrayInt, за исключением того, что мы добавили объявление параметра шаблона класса и изменили тип данных c int-а на T.

Обратите внимание, мы определили функцию getLength() вне тела класса. Это необязательно, но новички обычно спотыкаются на этом из-за синтаксиса. Каждый метод шаблона класса, объявленный вне тела класса, нуждается в собственном объявлении шаблона. Также обратите внимание, имя шаблона класса — Array<T>, а не Array (Array будет указывать на не шаблонную версию класса Array).

Вот пример использования шаблона класса Array:

Результат:

9     9.5
8     8.5
7     7.5
6     6.5
5     5.5
4     4.5
3     3.5
2     2.5
1     1.5
0     0.5

Шаблоны классов работают точно так же, как и шаблоны функций: компилятор копирует шаблон класса, заменяя типы параметров шаблона класса на фактические (передаваемые) типы данных, а затем компилирует эту копию. Если у вас есть шаблон класса, но вы его не используете, то компилятор не будет его даже компилировать.

Шаблоны классов идеально подходят для реализации контейнерных классов, так как очень часто таким классам приходится работать с разными типами данных, а шаблоны позволяют это организовать в минимальном количестве кода. Хотя синтаксис несколько уродлив, и сообщения об ошибках иногда могут быть «объёмными», шаблоны классов действительно являются одним из лучших и наиболее полезных свойств языка C++.

Шаблоны классов в Стандартной библиотеке С++

Теперь вы уже поняли, чем на самом деле является std::vector<int>? Правильно, std::vector – это шаблон класса, а int всего лишь передаваемый тип данных! Стандартная библиотека С++ полна предопределенных шаблонов классов, доступных для вашего использования. Мы рассмотрим эти классы в следующих главах.



Шаблоны классов и Заголовочные файлы

Шаблон не является ни классом, ни функцией — это трафарет, используемый для создания классов или функций. Таким образом, шаблоны работают не так, как обычные функции или классы. В большинстве случаев это не является проблемой, но на практике случаются разные ситуации.

Работая с обычными классами, мы помещаем определение класса в заголовочный файл, а определения методов этого класса в отдельный .cpp файл с аналогичным именем. Таким образом, фактическое определение класса компилируется как отдельный файл внутри проекта. Однако с шаблонами всё происходит несколько иначе (о том, почему следует помещать определение методов класса в отдельный файл, читайте в уроке №122). Рассмотрим следующее:

Array.h:

Array.cpp:

main.cpp:

Программа выше скомпилируется, но вызовет следующую ошибку линкера:

unresolved external symbol "public: int __thiscall Array::getLength(void)" (?GetLength@?$Array@H@@QAEHXZ)

Почему так? Сейчас разберемся.

Для использования шаблона, компилятор должен видеть как определение шаблона (а не только объявление), так и тип шаблона, используемый для создания экземпляра шаблона. Помним, что C++ компилирует файлы по отдельности. Когда заголовочный файл Array.h подключается в main.cpp, то определение шаблона класса копируется в этот файл. В main.cpp компилятор видит, что нам нужны два экземпляра шаблона класса: Array<int> и Array<double>, он создаст их, а затем скомпилирует весь этот код как часть файла main.cpp. Однако, когда дело дойдет до компиляции Array.cpp (отдельным файлом), компилятор забудет, что мы использовали Array<int> и Array<double> в main.cpp и не создаст экземпляр шаблона функции getLength(), который нам нужен для выполнения программы. Поэтому мы получим ошибку линкера, так как компилятор не сможет найти определение Array<int>::getLength() или Array<double>::getLength().

Эту проблему можно решить несколькими способами.

Самый простой вариант — поместить код из Array.cpp в Array.h, ниже класса. Таким образом, когда мы будем подключать Array.h, весь код шаблона класса (полное объявление и определение как класса, так и его методов) будет находиться в одном месте. Плюс этого способа – простота. Минус – если шаблон класса используется во многих местах, то мы получим много локальных копий шаблона класса, что увеличит время компиляции и линкинга файлов (линкер должен будет удалить дублирование определений класса и методов, дабы исполняемый файл «не был раздутым»). Мы рекомендуем использовать это решение до тех пор, пока время компиляции или линкинга не является проблемой.

Если вы считаете, что размещение кода Array.cpp в Array.h сделает Array.h слишком большим/беспорядочным, то альтернативой будет переименование Array.cpp в Array.inl (.inl означает inline (встроенный)), а затем подключение Array.inl из нижней части файла Array.h. Это даст тот же результат, что и размещение всего кода в заголовочном файле, но таким образом код получится немного чище.

Есть еще решения с подключением .cpp файлов, но эти варианты не рекомендуются из-за нестандартного использования директивы #include.

Еще один альтернативный вариант — использовать подход «трёх файлов»:

   Определение шаблона класса хранится в заголовочном файле.

   Определения методов шаблона класса хранятся в отдельном .cpp файле.

   Затем добавляем третий файл, который содержит все необходимые нам экземпляры шаблона класса.

Например:

templates.cpp:

Часть template class заставит компилятора явно создать указанные экземпляры шаблона класса. В примере выше компилятор создаст Array<int> и Array<double> внутри templates.cpp. Поскольку templates.cpp находится внутри нашего проекта, то он скомпилируется и удачно свяжется с другими файлами (пройдет линкинг).

Этот метод более эффективен, но требует создания/поддержки третьего файла (templates.cpp) для каждой из ваших программ (проектов) отдельно.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (3 оценок, среднее: 4,33 из 5)
Загрузка...

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++