19.6 – Частичная специализация шаблона для указателей
В предыдущем уроке «19.3 – Специализация шаблона функции» мы рассмотрели простой шаблонный класс Storage
:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
{
m_value = value;
}
~Storage()
{
}
void print()
{
std::cout << m_value << '\n';
}
};
Мы показали, что у этого класса были проблемы, когда параметр шаблона T
имел тип char*
из-за неглубокого копирования / присваивания указателя, которое происходит в конструкторе. В том уроке мы использовали полную специализацию шаблона для создания специализированной версии конструктора Storage
для типа char*
, который выделял память и выполнял глубокое копирование m_value
. Для справки, вот полностью специализированный конструктор и деструктор Storage
для char*
:
// Здесь вам необходимо включить класс Storage<T> из приведенного выше примера
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];
}
template<>
Storage<char*>::~Storage()
{
delete[] m_value;
}
Хотя это отлично работает для Storage<char*>
, как насчет других типов указателей (например, int*
)? Достаточно легко увидеть, что если T
является указателем любого типа, то мы сталкиваемся с проблемой, когда конструктор выполняет присваивание указателя вместо того, чтобы создавать фактическую глубокую копию элемента, на который тот указывает.
Поскольку полная специализация шаблонов заставляет нас полностью определять шаблонные типы, для решения этой проблемы нам нужно будет определить новый специализированный конструктор (и деструктор) для каждого типа указателя, с которым мы хотели бы использовать Storage
! Это приводит к появлению большого количества повторяющегося кода, которого, как вы уже хорошо знаете, мы хотим избежать, насколько это возможно.
К счастью, удобное решение нам предлагает частичная специализация шаблонов. В этом случае мы будем использовать частичную специализацию шаблона класса, чтобы определить специальную версию класса Storage
, которая работает со значениями указателей. Этот класс считается частично специализированным, потому что мы сообщаем компилятору, что он предназначен только для использования с указателями, даже если мы точно не указываем базовый тип.
#include <iostream>
// Здесь вам необходимо включить класс Storage<T> из приведенного выше примера
template <typename T>
// это частичная специализация Storage, которая работает с указателями
class Storage<T*>
{
private:
T* m_value;
public:
Storage(T* value) // для указателя T
{
// Для указателей выполняем глубокое копирование
m_value = new T(*value); // копирует одиночное значение, а не массив
}
~Storage()
{
// поэтому мы используем здесь обычное удаление, а не удаление массива
delete m_value;
}
void print()
{
std::cout << *m_value << '\n';
}
};
И пример работы с этим классом:
int main()
{
// Объявляем Storage не для указателя, чтобы показать, что он еще работает
Storage<int> myint(5);
myint.print();
// Объявляем Storage для указателя, чтобы показать, что он работает
int x = 7;
Storage<int*> myintptr(&x);
// Покажем, что myintptr отделен от x.
// Если мы изменим x, myintptr не должен измениться
x = 9;
myintptr.print();
return 0;
}
Этот код печатает следующие значения:
5
7
Когда myintptr
определяется с шаблонным параметром int*
, компилятор видит, что мы определили частично специализированный шаблон класса, который работает с любым типом указателя, и создает экземпляр Storage
, используя этот шаблон. Конструктор этого класса выполняет глубокое копирование параметра x
. Позже, когда мы изменим x
на 9, myintptr.m_value
не будет затронут, поскольку он указывает на свою собственную отдельную копию значения.
Если бы шаблона класса с частичной специализацией не было, myintptr
использовал бы обычную (не частично специализированную) версию шаблона. Конструктор этого класса выполнил бы поверхностное присваивание указателя копии, что означает, что myintptr.m_value
и x
ссылались бы на один и тот же адрес. Затем, когда мы изменили бы значение x
на 9, мы также изменили бы значение myintptr
.
Стоит отметить, что, поскольку этот частично специализированный класс Storage
размещает только одно значение, для строк в стиле C будет скопирован только первый символ. Если необходимо копировать целые строки, может быть выполнена полная специализация конструктора (и деструктора) для типа char*
. Полностью специализированная версия будет иметь приоритет перед частично специализированной версией. Вот пример программы, которая использует как частичную специализацию для указателей, так и полную специализацию для char*
:
#include <iostream>
#include <cstring>
// Наш класс Storage для не указателей
template <typename T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
{
m_value = value;
}
~Storage()
{
}
void print()
{
std::cout << m_value << '\n';
}
};
// Частичная специализация класса Storage для указателей
template <typename T>
class Storage<T*>
{
private:
T* m_value;
public:
Storage(T* value)
{
m_value = new T(*value);
}
~Storage()
{
delete m_value;
}
void print()
{
std::cout << *m_value << '\n';
}
};
// Полная специализация конструктора для типа 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];
}
// Полная специализация деструктора для типа char*
template<>
Storage<char*>::~Storage()
{
delete[] m_value;
}
// Полная специализация функции print для типа char*
// Без этого печать Storage<char*> вызвала бы Storage<T*>::print(),
// которая печатает только первый элемент
template<>
void Storage<char*>::print()
{
std::cout << m_value;
}
int main()
{
// Объявляем Storage для не указателя, чтобы показать, что всё работает
Storage<int> myint(5);
myint.print();
// Объявляем Storage для указателя, чтобы показать, что всё работает
int x = 7;
Storage<int*> myintptr(&x);
// Если myintptr присвоил указатель на x,
// тогда изменение x изменит и myintptr
x = 9;
myintptr.print();
// Динамически размещаем временную строку
char *name = new char[40]{ "Alex" }; // требует C++14
// Если ваш компилятор несовместим с C++14,
// закомментируйте строку выше и раскомментируйте следующие строки
// char *name = new char[40];
// strcpy(name, "Alex");
// Сохраняем name
Storage< char*> myname(name);
// Удаляем временную строку
delete[] name;
// Печатаем наше имя
myname.print();
}
Это работает так, как мы ожидали:
5
7
Alex
Использование частичной специализации шаблона класса для создания отдельных реализаций класса для указателя и не указателя чрезвычайно полезно, когда вы хотите, чтобы класс обрабатывал их разными способами, но полностью прозрачными для конечного пользователя.