19.3 – Специализация шаблона функции
При создании экземпляра шаблона функции для заданного типа компилятор создает копию шаблонной функции и заменяет шаблонные параметры типа фактическими типами, используемыми в объявлении переменной. Это означает, что каждая конкретная функция будет иметь такие же детали реализации, что и другие созданные экземпляры шаблона функции (только с использованием разных типов). Хотя в большинстве случаев это именно то, что вам нужно, иногда бывают случаи, когда полезно реализовать шаблонную функцию, немного отличающуюся для определенного типа данных.
Один из способов добиться этого – специализация шаблона.
Давайте посмотрим на очень простой шаблон класса:
template <typename T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
{
m_value = value;
}
void print()
{
std::cout << m_value << '\n';
}
};
Приведенный выше код отлично подходит для многих типов данных:
int main()
{
// Определяем несколько объектов Storage
Storage<int> nValue(5);
Storage<double> dValue(6.7);
// Печатаем значения
nValue.print();
dValue.print();
}
Этот код печатает:
5
6.7
Теперь предположим, что мы хотим, чтобы значения double
(и только значения double
) выводились в экспоненциальной записи. Для этого мы можем использовать специализацию шаблона функции (иногда называемую полной или явной специализацией шаблона функции), чтобы создать специализированную версию функции print()
для типа double
. Это очень просто: просто определите специализированную функцию (если функция является функцией-членом, сделайте это вне определения класса), заменив шаблонный тип конкретным типом, для которого вы хотите переопределить функцию. Вот наша специализированная функция print()
для double
:
template <>
void Storage<double>::print()
{
std::cout << std::scientific << m_value << '\n';
}
Когда компилятор переходит к созданию экземпляра Storage<double>::print()
, он увидит, что мы уже явно определили эту функцию, и будет использовать версию, которую определили мы, вместо того, чтобы создавать версию из обобщенного образца из шаблонного класса.
template<>
сообщает компилятору, что это шаблон функции, но у которой нет никаких шаблонных параметров (поскольку в этом случае мы явно указываем все типы). Некоторые компиляторы могут позволить вам опустить эту запись, но с ней будет правильнее.
В результате, когда мы повторно запустим приведенную выше программу, она напечатает:
5
6.700000e+000
Еще один пример
Теперь давайте рассмотрим еще один пример, в котором может быть полезна специализация шаблонов. Подумайте, что произойдет, если мы попытаемся использовать наш шаблонный класс Storage
с типом данных const char*
:
#include <iostream>
#include <string>
template <typename T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
{
m_value = value;
}
void print()
{
std::cout << m_value << '\n';
}
};
int main()
{
// Динамически размещаем временную строку
std::string s;
// Спрашиваем у пользователя его имя
std::cout << "Enter your name: ";
std::cin >> s;
// Сохраняем имя
Storage<char*> storage(s.data());
storage.print(); // печатает наше имя
s.clear(); // очищаем std::string
storage.print(); // ничего не печатает
}
Как оказалось, вместо того, чтобы печатать имя, второй вызов функции storage.print()
ничего не печатает! Что тут происходит?
Когда Storage
создается для типа char*
, конструктор для Storage<char*>
выглядит следующим образом:
template <>
Storage<char*>::Storage(char* value)
{
m_value = value;
}
Другими словами, это просто присваивание указателя (поверхностное копирование)! В результате m_value
указывает на то же место в памяти, что и строка. Когда мы удаляем строку в main()
, мы в конечном итоге удаляем значение, на которое указывал m_value
! Таким образом, при попытке распечатать это значение мы получаем мусор.
К счастью, мы можем решить эту проблему, используя специализацию шаблонов. Вместо копирования указателя нам на самом деле нужно, чтобы наш конструктор делал копию входной строки. Итак, давайте напишем специальный конструктор для типа данных char*
, который делает именно это:
template <>
Storage<char*>::Storage(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];
}
Теперь, когда мы размещаем переменную типа Storage<char*>
, этот конструктор будет использоваться вместо конструктора по умолчанию. В результате m_value
получит собственную копию строки. Следовательно, когда мы удалим строку, m_value
не изменится.
Однако у этого класса теперь есть утечка памяти для типа char*
, потому что m_value
не будет удалена, когда переменная Storage
выходит за пределы области видимости. Как вы уже догадались, это также можно решить, специализировав деструктор Storage<char*>
:
template <>
Storage<char*>::~Storage()
{
delete[] m_value;
}
Теперь, когда переменные типа Storage<char*>
выходят за пределы области видимости, память, выделенная в специализированном конструкторе, будет удалена в специализированном деструкторе.
Хотя в приведенных выше примерах используются только функции-члены, вы можете точно так же специализировать шаблоны функций, не являющихся членами.