На этом уроке мы рассмотрим, что такое частичная специализация шаблона в языке С++, как она используется и какие есть нюансы.
Проблема
На уроке №176 мы узнали, каким образом можно использовать дополнительный параметр шаблона. Рассмотрим еще раз класс StaticArray из материалов того же урока:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template <class T, int size> // size является non-type параметром шаблона class StaticArray { private: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; |
Здесь у нас есть 2 параметра шаблона класса: параметр типа и параметр non-type.
Теперь предположим, что нам нужно написать функцию для вывода всех элементов массива. Хотя мы можем сделать это через метод класса, мы реализуем это через отдельную функцию (ради лучшего погружения в тему).
Используя шаблон функции, мы можем написать следующее:
1 2 3 4 5 6 |
template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count=0; count < size; ++count) std::cout << array[count] << ' '; } |
Это позволит нам сделать:
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 |
#include <iostream> #include <cstring> template <class T, int size> // size является non-type параметром шаблона class StaticArray { private: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count] << ' '; } int main() { // Объявляем целочисленный массив StaticArray<int, 5> int5; int5[0] = 0; int5[1] = 1; int5[2] = 2; int5[3] = 3; int5[4] = 4; // Выводим элементы массива print(int5); return 0; } |
И получить:
0 1 2 3 4
Хотя всё работает правильно, но есть один нюанс. Рассмотрим следующий код функции main():
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() { // Объявляем массив типа char StaticArray<char, 14> char14; strcpy_s(char14.getArray(), 14, "Hello, world!"); // Выводим элементы массива print(char14); return 0; } |
Примечание: Мы рассматривали strcpy_s на уроке о строках C-style.
Программа скомпилируется со следующим результатом:
H e l l o , w o r l d !
Для всех типов, кроме char, имеет смысл помещать пробел между каждым элементом массива, чтобы элементы не «слипались». Однако с типом char есть смысл вывести всё вместе, как строку C-style, чтобы не было лишних пробелов. Как мы можем это исправить?
Полная специализация шаблона — решение?
Сначала мы могли бы подумать об использовании специализации шаблона функции. Однако проблема с полной специализацией шаблона заключается в том, что все параметры шаблона должны быть явно определены. Например:
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> #include <cstring> template <class T, int size> // size является non-type параметром шаблона class StaticArray { private: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count] << ' '; } // Шаблон функции print() с полной специализацией шаблона класса StaticArray для работы с типом char и длиной массива 14 template <> void print(StaticArray<char, 14> &array) { for (int count = 0; count < 14; ++count) std::cout << array[count]; } int main() { // Объявляем массив типа char StaticArray<char, 14> char14; strcpy_s(char14.getArray(), 14, "Hello, world!"); // Выводим элементы массива print(char14); return 0; } |
Как вы можете видеть, мы добавили шаблон функции print() для работы с типом char. Результат:
Hello, world!
Хотя одна проблема решена, возникает другая проблема: использование полной специализации шаблона класса означает, что мы должны явно указывать длину передаваемого массива! Рассмотрим следующий пример:
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() { // Объявляем массив типа char StaticArray<char, 12> char12; strcpy_s(char12.getArray(), 12, "Hello, dad!"); // Выводим элементы массива print(char12); return 0; } |
Вызов print(char12)
вызовет шаблон функции print() с общим шаблоном StaticArray<T, size>
, так как char12 является типа StaticArray<char, 12>
, а шаблон функции print() принимает только StaticArray<char, 14>
(длина массива отличается).
Хотя мы могли бы скопировать еще раз шаблон функции print() для работы со StaticArray<char, 12>
, но это неэффективно. А что, если нам нужно будет позднее использовать массив с 5 или 20 элементами? Опять копировать шаблон? Это лишняя работа.
Очевидно, что полная специализация шаблона класса здесь является решением-костылем. Частичная специализация шаблона — вот, что нам нужно.
Частичная специализация шаблона
Частичная специализация шаблона позволяет выполнить специализацию шаблона класса (но не функции!), где некоторые (но не все) параметры шаблона явно определены. Для нашей вышеприведенной задачи идеальное решение заключается в том, чтобы шаблон функции print() работал со StaticArray типа char, но при этом размер массива не являлся фиксированным значением, а мог варьироваться.
Вот наш шаблон функции print(), который принимает частично специализированный шаблон класса StaticArray:
1 2 3 4 5 6 7 |
// Шаблон функции print() с частично специализированным шаблоном класса StaticArray<char, size> в качестве параметра template <int size> // size по-прежнему является non-type параметром void print(StaticArray<char, size> &array) // мы здесь явно указываем тип char { for (int count = 0; count < size; ++count) std::cout << array[count]; } |
Как вы можете видеть, мы здесь явно указали тип char, но size
оставили не фиксированным, поэтому функция print() будет работать с массивами типа 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 |
#include <iostream> #include <cstring> template <class T, int size> // size является non-type параметром шаблона class StaticArray { private: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count] << ' '; } // Шаблон функции print() с частично специализированным шаблоном класса StaticArray<char, size> в качестве параметра template <int size> void print(StaticArray<char, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count]; } int main() { // Объявляем массив типа char длиной 14 StaticArray<char, 14> char14; strcpy_s(char14.getArray(), 14, "Hello, world!"); // Выводим элементы массива print(char14); // Теперь объявляем массив типа char длиной 12 StaticArray<char, 12> char12; strcpy_s(char12.getArray(), 12, "Hello, dad!"); // Выводим элементы массива print(char12); return 0; } |
Результат:
Hello, world! Hello, dad!
Как и ожидалось.
Обратите внимание, начиная с C++14 частичная специализация шаблона может использоваться только с классами, но не с отдельными функциями (для функций используется только полная специализация шаблона). Наш пример void print(StaticArray<char, size> & array)
работает только потому, что шаблон функции print() принимает в качестве параметра шаблон класса, который, в свою очередь, частично специализирован.
Частичная специализация шаблонов методов
Ограничение частичной специализации для функций может привести к некоторым проблемам при работе с методами класса. Например, что, если бы мы определили StaticArray следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
template <class T, int size> // size является non-type параметром шаблона class StaticArray { private: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } void print() { for (int i = 0; i < size; i++) std::cout << m_array[i] << ' '; std::cout << "\n"; } }; |
Функция print() является методом класса StaticArray<T, int>
. Что произойдет, если мы захотим частично специализировать шаблон функции print(), чтобы метод работал по-другому? Мы можем попробовать сделать следующее:
1 2 3 4 5 6 7 8 |
// Не сработает template <int size> void StaticArray<double, size>::print() { for (int i = 0; i < size; i++) std::cout << std::scientific << m_array[i] << " "; std::cout << "\n"; } |
К сожалению, это не сработает, так как мы пытаемся частично специализировать шаблон функции, что делать запрещено.
Как же это можно обойти? Одним из очевидных решений является частичная специализация шаблона всего класса:
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 |
#include <iostream> template <class T, int size> // size является non-type параметром шаблона class StaticArray { private: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } void print() { for (int i = 0; i < size; i++) std::cout << m_array[i] << ' '; std::cout << "\n"; } }; template <int size> // size является non-type параметром шаблона class StaticArray<double, size> { private: // Параметр size отвечает за длину массива double m_array[size]; public: double* getArray() { return m_array; } double& operator[](int index) { return m_array[index]; } void print() { for (int i = 0; i < size; i++) std::cout << std::scientific << m_array[i] << ' '; std::cout << "\n"; } }; int main() { // Объявляем целочисленный массив длиной 5 StaticArray<int, 5> intArray; // Заполняем массив, а затем выводим его for (int count = 0; count < 5; ++count) intArray[count] = count; intArray.print(); // Объявляем массив типа double длиной 4 StaticArray<double, 4> doubleArray; for (int count = 0; count < 4; ++count) doubleArray[count] = (4.0 + 0.1 * count); doubleArray.print(); return 0; } |
Результат:
0 1 2 3 4
4.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00
Хотя это работает, но это не самый лучший вариант, так как у нас теперь куча дублированного кода из StaticArray<T, size>
в StaticArray<double, size>
.
Если бы можно было использовать код из StaticArray<T, size>
в StaticArray<double, size>
без дублирования. Ничего вам это не напоминает? Как по мне, то это звучит, как отличный вариант для применения наследования!
Вы можете начать с:
1 2 |
template <int size> // size является non-type параметром шаблона class StaticArray<double, size>: public StaticArray< // а затем что? |
Но как мы можем ссылаться на StaticArray? Никак, но, к счастью, есть обходной путь с использованием общего родительского класса:
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 |
#include <iostream> template <class T, int size> // size является non-type параметром шаблона class StaticArray_Base { protected: // Параметр size отвечает за длину массива T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } virtual void print() { for (int i = 0; i < size; i++) std::cout << m_array[i] << ' '; std::cout << "\n"; } }; template <class T, int size> // size является non-type параметром шаблона class StaticArray: public StaticArray_Base<T, size> { public: StaticArray() { } }; template <int size> // size является non-type параметром шаблона class StaticArray<double, size>: public StaticArray_Base<double, size> { public: virtual void print() override { for (int i = 0; i < size; i++) std::cout << std::scientific << this->m_array[i] << " "; // Примечание: Префикс this-> на вышеприведенной строке необходим. Почему? Читайте здесь - https://stackoverflow.com/a/6592617 std::cout << "\n"; } }; int main() { // Объявляем целочисленный массив длиной 5 StaticArray<int, 5> intArray; // Заполняем его, а затем выводим for (int count = 0; count < 5; ++count) intArray[count] = count; intArray.print(); // Объявляем массив типа double длиной 4 StaticArray<double, 4> doubleArray; // Заполняем его, а затем выводим for (int count = 0; count < 4; ++count) doubleArray[count] = (4. + 0.1*count); doubleArray.print(); return 0; } |
Результат тот же, что и в примере, приведенном выше, но дублированного кода меньше.
Может, я что-то недопонимаю, но частичная специализация шаблона функции(!) у меня работает (стандарт в настройках c++14):
Вывод:
4
3
Partial
Partial
Это работает только потому, что шаблон функции принимает в качестве параметра шаблон класса, который, в свою очередь, частично специализирован. (с)
Это не частичная специализация, это перегрузка. Просто одна перегрузка использует один шаблон, а вторая перегрузка использует другой шаблон. Это можно проверить убрав верхнюю функцию, и нижняя нормально вызовется. Была бы частичной специализацией, то была бы ошибка компиляции)
Зачем в последнем примере с наследованием метод print() делается виртуальным? Не проще ли его просто переопределить?
А почему в частичной специализации методов класса, при использовании наследования здесь:
мы не можем наследовать исходный шаблон?
Вместо использования дополнительного родительского StaticArray_Base
Правильно я понимаю, что в примере
мы по сути одним шаблоном "перегрузили" другой.
В начале урока проскальзывала такая функция,как strcpy_s,принимающая массив,в который мы хотим что то записать,длину этого массива и собственно то,что мы хотим записать,но эта функция вызывала ошибку компиляции.
Решением было использовать функцию strcpy,которая принимает то,куда мы хотим записать,и то,что мы хотим записать.
Вопрос вот в чём,откуда взялась функция strcpy?В компиляторе на GCC на Code::Blocks такой даже нету в наличии.
Она находится в составе системной библиотеки cstring.
однако, в уроке про строки в стиле С функция strcpy_s() принимала всего два параметра — массив, в который копируем и массив, который копируем. Не лишний ли в примере из этого урока второй параметр?
Второй аргумент в данном случае это размер приемника(максимальное количество символов которых мы можем записать )
https://en.cppreference.com/w/c/string/byte/strcpy
Почему специализация
является полной, а не частичной?
Это не специализация, это шаблон функции print() с одним параметром non-type. Спасибо, исправил.