При создании экземпляра шаблона функции для определенного типа данных компилятор копирует шаблон функции и заменяет параметр типа шаблона функции на фактический (передаваемый) тип данных. Это означает, что все экземпляры функции имеют одну реализацию, но разные типы данных. Хотя в большинстве случаев это именно то, что требуется, иногда может понадобиться, чтобы реализация шаблона функции для одного типа данных отличалась от реализации шаблона функции для другого типа данных.
Специализация шаблонов именно для этого и предназначена.
Рассмотрим очень простой шаблон класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; |
Вышеприведенный код работает со многими типами данных:
1 2 3 4 5 6 7 8 9 10 |
int main() { // Инициализируем объекты класса Repository<int> nValue(7); Repository<double> dValue(8.4); // Выводим значения объектов класса nValue.print(); dValue.print(); } |
Результат:
7
8.4
Теперь, предположим, что нам нужно, чтобы значения типа double (только типа double) выводились в экспоненциальной записи. Для этого мы можем использовать специализацию шаблона функции (или «полную/явную специализацию шаблона функции») для создания отдельной версии функции print() для вывода значений типа double.
Всё просто: записываем экземпляр шаблона функции (если функция является методом класса, то делаем это за пределами класса), указывая нужный нам тип данных. Например, вот специальный шаблон функции print() для значений типа double:
1 2 3 4 5 |
template <> void Repository<double>::print() { std::cout << std::scientific << m_value << '\n'; } |
Когда компилятору нужно будет создать экземпляр Repository<double>::print()
, он увидит, что мы уже явно определили эту функцию, и поэтому он будет использовать именно этот экземпляр, а не копировать общую для всех типов данных версию шаблона функции print().
Часть template <>
сообщает компилятору, что это шаблон функции, но без параметров (так как в этом случае мы явно указываем нужный нам тип данных).
Результат выполнения программы:
7
8.400000e+00
Еще один пример
Рассмотрим еще один случай, где специализация шаблонов функций может быть полезна. Например, что произойдет, если мы попытаемся использовать наш шаблон класса Repository с типом данных char*?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
int main() { // Динамически выделяем временную строку char *string = new char[40]; // Просим пользователя ввести свое имя std::cout << "Enter your name: "; std::cin >> string; // Сохраняем то, что ввел пользователь Repository<char*> repository(string); // Удаляем временную строку delete[] string; // Пытаемся вывести то, что ввел пользователь repository.print(); // получаем мусор } |
Оказывается, вместо вывода имени пользователя, repository.print()
выведет мусор! Почему?
При создании экземпляра шаблона для типа char*, конструктор Repository<char*>
выглядит следующим образом:
1 2 3 4 5 |
template <> Repository<char*>::Repository(char* value) { m_value = value; } |
Другими словами, это просто присваивание указателя (поверхностное копирование)! В результате m_value
указывает на ту же область памяти, что и переменная string
. А когда мы удаляем string
в main(), то мы удаляем значение, на которое указывает и m_value
! Таким образом, происходит утечка памяти, и мы получаем мусор при попытке вывода m_value
.
К счастью, мы можем это исправить, используя явную специализацию шаблона функции. Вместо копирования указателя на string
, нам нужно, чтобы конструктор копировал само значение string
. Напишем отдельный конструктор для типа данных char*, который будет именно это и делать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template <> Repository<char*>::Repository(char* value) { // Определяем длину value int length=0; while (value[length] != '\0') ++length; ++length; // +1, учитывая нуль-терминатор // Выделяем память для хранения значения value m_value = new char[length]; // Копируем фактическое значение value в m_value for (int count=0; count < length; ++count) m_value[count] = value[count]; } |
Теперь при выделении переменной типа Repository<char*>
именно этот конструктор будет использоваться вместо стандартного. В результате m_value
получит свою собственную копию string
. Следовательно, когда мы удалим string
в main(), m_value
это никак не заденет.
Однако, теперь класс имеет утечку памяти для типа char*, поскольку m_value
не будет удален, когда переменная repository
выйдет из области видимости. Как вы уже могли догадаться, это также можно решить, сделав отдельный деструктор для типа char*:
1 2 3 4 5 |
template <> Repository<char*>::~Repository() { delete[] m_value; } |
Теперь, когда переменные типа Repository<char*>
выйдут из области видимости, память, выделенная в специальном конструкторе, будет удалена в специальном деструкторе.
Хотя во всех примерах, приведенных выше, мы работаем с методами класса, вы также можете аналогично выполнять явную специализацию шаблонов обычных функций (которые не являются методами классов).
Здравствуйте , возник небольшой вопрос, по поводу специализации шаблона для функции print(), я хочу чтобы при вызове этой функции массив выводился, как я могу это сделать?
Так он выводится
Или так?
К автору перевода и сайта конечно нет претензий, но автору книги урок стоило бы назвать: "Явная специализация шаблона функции класса". Ибо если есть шаблон и есть один тип который надо сделать по особому, то получается надо делать просто обычную перегруженную функцию. Других примеров здесь нет. Например если особая функция преобразует в теле число в строку через to_string.
Вообще автор закрутил конечно свою фразочку: "Всё просто: записываем экземпляр шаблона функции (если функция является методом класса…". Прям как хитроопый юрист. А если не является?! То угадайте сами. Даже с начинающими программистами такие фразочки не проходят, ибо они быстро упрутся в проблему. Такое чувство, что он сам не знал как поступать в таком случае, но пытался это завуалировать.
Стоило хотя бы написать, что может стоит в таком случае перегруженную функцию поставить первой, до шаблона. Чтобы компилятор сразу находил её, а не пытался создать её из шаблона. Но это всё теперь догадки.
Может автор перевода уже знает больше? Было бы полезноно узнать.
А еще есть функции strlen, strcpy, strncpy
Для этих функций нужно заголовочный файл подключать, а оно нам надо?
Не учите плохому. Кончено, нужны библиотечные функции (не жадничаем на один лишний заголовочный файл). В статье не какой-нибудь стек-массив, а выделение динамической памяти. А тут уже может быть принципиальная разность скорости копирования с помощью цикла и memcpy (условный гигабайт выделенной памяти)
Я бы сказал не утечка памяти, а dangling pointer.