На уроке о специализации шаблона функции мы рассматривали шаблон класса Repository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; |
Мы говорили о проблеме этого шаблона при работе с типом char*, когда выполнялось поверхностное копирование (присваивание указателя) в конструкторе класса Repository. В качестве решения мы использовали полную специализацию шаблона для создания специализированной версии конструктора класса Repository для работы с типом char*, в котором выделялась память и выполнялось глубокое копирование m_value
. Вот специализация конструктора и деструктора класса Repository для работы с типом char* (из материалов того же урока):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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]; } template<> Repository<char*>::~Repository() { delete[] m_value; } |
Хотя всё отлично работает с типом char*, но как насчет других типов указателей (например, int*)? Поскольку T
— это любой тип указателя, то при работе с тем же int* выполнится поверхностное копирование (что нам не нужно), либо нам придется дублировать вышеприведенный код (специализация конструктора и деструктора), но уже вместо char* использовать int*. А дублирование кода, как мы уже знаем, не самый лучший вариант!
К счастью, используя частичную специализацию шаблона, мы можем определить специальную версию класса Repository, которая работала бы со всеми типами указателей (при этом не нужно указывать конкретные типы указателей):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <iostream> // Общий шаблон класса Repository template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; template <typename T> class Repository<T*> // частичная специализация шаблона класса Repository для работы с типами указателей { private: T* m_value; public: Repository(T* value) // T - тип указателя { // Выполняем глубокое копирование m_value = new T(*value); // здесь копируется только одно отдельное значение (не массив значений) } ~Repository() { delete m_value; // а здесь выполняется удаление этого значения } void print() { std::cout << *m_value << '\n'; } }; |
И пример из практики:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int main() { // Объявляем целочисленный объект для проверки работы общего шаблона класса Repository<int> myint(6); myint.print(); // Объявляем объект с типом указатель для проверки работы частичной специализации шаблона класса int x = 8; Repository<int*> myintptr(&x); // Если бы в myintptr выполнилось поверхностное копирование (присваивание указателя), // то изменение значения x изменило бы и значение myintptr x = 10; myintptr.print(); return 0; } |
Результат:
6
8
При объявлении объекта myintptr
с типом int*, компилятор видит, что мы ранее определили частичную специализацию шаблона класса для работы с типами указателей, и, учитывая, что мы использовали тип int*, компилятор создаст экземпляр частичной специализации шаблона для работы с типом указателя. Конструктор этой специализации выполняет глубокое копирование параметра x
. Позже, когда мы изменяем значение x
на 10
, myintptr.m_value
никак не задевается, так как выполнилось глубокое копирование, при котором m_value
получил свою собственную копию x
.
Если бы этой частичной специализации не существовало, то создался бы экземпляр общего шаблона класса, в котором выполнилось бы поверхностное копирование, а myintptr.m_value
и x
указывали бы на один и тот же адрес памяти. В таком случае, при изменении значения переменной x
на 10
, мы также затронули бы и значение myintptr
(оно также стало бы равно 10
).
Стоит отметить, что, поскольку в нашей частичной специализации копируется только одно значение, при работе со строками C-style копироваться будет только первый символ (так как строка — это массив, а указатель на массив указывает только на первый элемент массива). Если же нужно полностью скопировать строку, то специализация конструктора (и деструктора) для типа char* должна быть полной. В таком случае, полная специализация будет иметь приоритет выше, чем частичная специализация. Например, вот программа, в которой используется как частичная специализация для работы с типами указателей, так и полная специализация для работы с типом char*:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
#include <iostream> #include <cstring> // Общий шаблон класса Repository для работы с не указателями template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; // Частичная специализация шаблона класса Repository для работы с указателями template <class T> class Repository<T*> { private: T* m_value; public: Repository(T* value) { m_value = new T(*value); } ~Repository() { delete m_value; } void print() { std::cout << *m_value << '\n'; } }; // Полная специализация шаблона конструктора класса Repository для работы с типом char* 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* template<> Repository<char*>::~Repository() { delete[] m_value; } // Полная специализация шаблона метода print() для работы с типом char*. // Без этого вывод Repository<char*> привел бы к вызову Repository<T*>::print(), который выводит только одно значение (в случае со строкой C-style - только первый символ) template<> void Repository<char*>::print() { std::cout << m_value; } int main() { // Объявляем целочисленный объект для проверки работы общего шаблона класса Repository<int> myint(6); myint.print(); // Объявляем объект с типом указатель для проверки работы частичной специализации шаблона int x = 8; Repository<int*> myintptr(&x); // Если бы в myintptr выполнилось поверхностное копирование (присваивание указателя), // то изменение значения x изменило бы и значение myintptr x = 10; myintptr.print(); // Динамически выделяем временную строку char *name = new char[40]{ "Anton" }; // необходим C++14 // Если ваш компилятор не поддерживает C++14, то закомментируйте строку выше и раскомментируйте строки, приведенные ниже // char *name = new char[40]; // strcpy(name, "Anton"); // Сохраняем имя Repository<char*> myname(name); // Удаляем временную строку delete[] name; // Выводим имя myname.print(); } |
Всё работает как нужно:
6
8
Anton
В итоге, использование частичной специализации шаблона класса для работы с типами указателей особенно полезно, так как позволяет предусмотреть все возможные варианты использования кода на практике.
Таки получилось сделать Родительский шаблон! Убрал специализацию для char для краткости. Данная реализация предотвращает дублирование кода:
Результат:
6
8
Подскажи почему не получается инициализировать родительский конструктор через список инициализации при использовании шаблона класса?
Но тут опять же проблема дублирующего кода возникает (у общего шаблона и специализации указателей)… И в данном случае, похоже, поделать с этим нечего. Родитеский шаблон придумать не получилось… m_value не может быть одновременно указателем и переменной.