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

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 87934

 ǀ   12 

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

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

На уроке о контейнерных классах мы узнали то, как, используя композицию, реализовать классы, содержащие несколько объектов определенного типа данных. В качестве примера мы использовали класс 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 (182 оценок, среднее: 4,89 из 5)
Загрузка...

Комментариев: 12

  1. Finchi:

    Еще вопрос: при использовании подхода "трех файлов", если один из параметров шаблона non-type, можно ли оставить этот параметр без конкретного значения и каким образом? Например класс Array <class T, int size>

  2. Finchi:

    Можно ли при использовании "подхода трех файлов" чтобы не нарушать "правила хорошего тона" переименовать Array.cpp в Array.inl, это будет лучшим решением?

  3. Алексей:

    Ох как же мне помог этот урок… Но по моей невнимательности я быстренько досмотрел до момента, где написано, как в cpp файле определить метод класса, но не прочитал то, что такой метод выдаст ошибку, в итоге час искал в инете решение (вкладка с этим уроком была открыта) и там бело написано, что скорее всего просто неправильно подключили заголовочные файлы. Я уже даже убрал все шаблоны из файлов, и тогда заработало. Когда-же я прочитал этот момент в этом уроке, в комнате раздался очень громкий шлепок…

  4. Константин:

    Если просто добавить экземпляры шаблона класса после определения в файл Array.cpp, то так же будет работать и третий файл не понадобится:

  5. Kris:

    А что мешает добавить в препроцессор возможность указывать определенные файлы за счет какой-нибудь директив так, чтобы препроцессор проанализировал файл, нашел места вызова шаблонных функций и классов, и добавил их в соответствующие файлы. Эту штуку можно и самому запилить. Таким образом, у нас отпадает необходимость самим постоянно контролировать запись инстанцирования тех типов, которые мы используем в файле и записывать их в отдельный .cpp файл, и увеличиваем скорость компиляции кода в несколько раз в реально больших проектах.

    1. Алексей:

      Хотелось бы по подробнее.

  6. Pavel:

    С .inl-файлами как-то расплывчато написано (в английском оригинале то же самое). Если кому-то интересно, то если подключить просто переименовать файл "Array.cpp" в "Array.inl" и прописать внизу файла Array.h следующую строку:

    программа не скомпилируется. В "Array.inl" нужно добавить header-guard. Причём #pragma once с inl-файлами у меня почему-то не работает.

    1. Bampi:

      Возможно что еще актуально. Решение нашел в "C++ Cookbook", D.Ryan Stephens в главе 2.5 Including an inline File.

      Array.h

      В созданном файле array.inl:

      и так далее.
      Все должно скомпилироваться и работать.

      1. Виталий:

        в файле Array.inl -> Чтобы не получить ошибку компиляции с двумя объявлениями

    2. Aleksandr:

      Так же, чтобы при подключение inl файла не происходило переопределения метода, можно исключить файл из сборки, "правой кнопкой мыши по файлу в обозревателе решений -> свойства -> обшие -> исключен из сборки -> да -> применить".

  7. Андрей:

    Попробовал сделать шаблон класса и распихал его в Fraction.h и Fraction.cpp. Попробовал все три способа компиляции и получил ошибку:

    ссылка не неразрешённый символ int_cdecl nod(int, int)
    ссылка на неразрешённый символ class Fraction<int> const&_cdecl pperator+(class Fraction<int> const&,
    class Fraction<int> const&)

    Что делать?

    1. Анастасия:

      похоже, что проблема в типе int_cdecl, можете погуглить, Вы не одиноки с этой проблемой. Если ещё актуально, конечно.

Добавить комментарий для Андрей Отменить ответ

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