19.5 – Частичная специализация шаблона

Добавлено 25 августа 2021 в 01:33

Этот и следующий урок не обязательны для прочтения и предназначены для тех, кто хочет получить более глубокие знания о шаблонах в C++. Частичная специализация шаблонов используется не так часто (но в определенных случаях может быть полезна).

В уроке «19.2 – Шаблонные параметры, не являющиеся типами данных» вы узнали, как можно использовать параметры-выражения для параметризации шаблонов классов.

Давайте еще раз посмотрим на класс StaticArray, который мы использовали в одном из наших предыдущих примеров:

template <typename T, int size> // size - параметр-выражение
class StaticArray
{
private:
    // Этот параметр-выражение управляет размером массива
    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] << ' ';
}

Это позволит нам сделать следующее:

#include <iostream>
#include <cstring>
 
template <typename T, int size> // size - параметр-выражение
class StaticArray
{
private:
	// Этот параметр-выражение управляет размером массива
	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()
{
	// объявляем массив int
	StaticArray<int, 4> int4{};
	int4[0] = 0;
	int4[1] = 1;
	int4[2] = 2;
	int4[3] = 3;
 
	// распечатываем массив
	print(int4);
 
	return 0;
}

и получаем следующий результат:

0 1 2 3

Хотя это работает, у этого решения есть недостаток в дизайне. Рассмотрим следующий код:

int main()
{
    // объявляем массив char
    StaticArray<char, 14> char14{};
 
    std::strcpy(char14.getArray(), "Hello, world!");
 
    // распечатываем массив
    print(char14);
 
    return 0;
}

(Если вам нужно освежить память, то std::strcpy мы рассмотрели в уроке «10.6 – Строки в стиле C».)

Эта программа скомпилируется, выполнится и выдаст следующее значение (или подобное):

H e l l o ,   w o r l d !

Для типов, не являющихся символами, имеет смысл вставлять пробел между элементами массива, чтобы они не шли вместе. Однако с типом char может быть лучше печатать всё вместе как строку в стиле C, чего наша функция print() не делает.

Итак, как мы можем это исправить?

Специализация шаблонов приходит на помощь?

Сначала можно подумать об использовании специализации шаблонов. Проблема с полной специализацией шаблона заключается в том, что все параметры шаблона должны быть определены явно.

Рассмотрим:

#include <iostream>
#include <cstring>
 
template <typename T, int size> // size - параметр-выражение
class StaticArray
{
private:
	// Этот параметр-выражение управляет размером массива
	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{};
 
    std::strcpy(char14.getArray(), "Hello, world!");
 
    // распечатываем массив
    print(char14);
 
    return 0;
}

Как видите, теперь мы предоставили перегруженную функцию печати для полностью специализированного StaticArray<char, 14>. Действительно, этот код печатает:

Hello, world!

Хотя это решает проблему обеспечения возможности вызова print() с StaticArray<char, 14>, возникает другая проблема: использование полной специализации шаблона означает, что мы должны явно определить длину массива, который будет принимать эта функция! Рассмотрим следующий пример:

int main()
{
    // объявляем массив char 
    StaticArray<char, 12> char12{};
 
    std::strcpy(char12.getArray(), "Hello, mom!");
 
    // распечатываем массив
    print(char12);
 
    return 0;
}

Вызов print() с char12 вызовет версию print(), которая принимает StaticArray<T, size>, потому что char12 имеет тип StaticArray<char, 12>, а наша перегруженная функция print() будет вызываться только при передаче StaticArray<char, 14>.

Хотя мы могли бы сделать копию print(), которая обрабатывает StaticArray<char, 12>, но что произойдет, когда мы захотим вызвать print() с размером массива 5 или 22? Нам нужно будет скопировать эту функцию для каждого размера массива. А это избыточно.

Очевидно, что полная специализация шаблона здесь является слишком ограничивающим решением. Решение, которое мы ищем, – это частичная специализация шаблона.

Частичная специализация шаблона

Частичная специализация шаблона позволяет нам специализировать классы (но не отдельные функции!), где некоторые, но не все параметры шаблона были определены явно. Для нашей задачи, описанной выше, идеальным решением было бы, чтобы наша перегруженная функция печати работала со StaticArray типа char, но оставила параметр-выражение длины так, чтобы его можно было изменять при необходимости. Частичная специализация шаблонов позволяет нам это сделать!

Вот наш пример с перегруженной функцией печати, которая принимает частично специализированный StaticArray:

// перегрузка функции print() для частично специализированного StaticArray<char, size>
template <int size> // size по-прежнему является параметром-выражением шаблона
void print(StaticArray<char, size>& array) // здесь мы явно определяем тип char
{
	for (int count{ 0 }; count < size; ++count)
		std::cout << array[count];
}

Как вы можете здесь видеть, мы явно объявили, что эта функция будет работать только для StaticArray типа char, но size по-прежнему является параметром-выражением шаблона, поэтому функция будет работать для массивов char любого размера. Вот и все!

Вот полная программа, использующая этот шаблон:

#include <iostream>
#include <cstring>
 
template <typename T, int size> // size - параметр-выражение
class StaticArray
{
private:
	// Этот параметр-выражение управляет размером массива
	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{};
 
	std::strcpy(char14.getArray(), "Hello, world!");
 
	// распечатываем массив
	print(char14);
 
	std::cout << ' ';
 
	// Теперь объявляем массив char размером 12
	StaticArray<char, 12> char12{};
 
	std::strcpy(char12.getArray(), "Hello, mom!");
 
	// распечатываем массив
	print(char12);
 
	return 0;
}

Эта программа печатает:

Hello, world! Hello, mom!

Как мы и ожидали.

Частичная специализация шаблона может использоваться только с классами, но не с шаблонами функций (функции должны быть полностью специализированными). Наш пример void print(StaticArray<char, size> &array) работает, потому что функция печати не является частично специализированной (это просто перегруженная функция, использующая частично специализированный параметр типа класса).

Частичная специализация шаблона для функций-членов

Ограничение частичной специализации функций может привести к некоторым проблемам при работе с функциями-членами. Например, что, если бы мы определили StaticArray так?

template <typename T, int size> // size - параметр-выражение
class StaticArray
{
private:
    // Этот параметр-выражение управляет размером массива
    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(), чтобы она работала по-другому? Вы можете попробовать это:

// Не работает
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';
}

К сожалению, это не работает, потому что мы пытаемся частично специализировать функцию, что запрещено.

Итак, как нам это обойти? Один из очевидных способов – частично специализировать весь класс:

#include<iostream>
 
template <typename T, int size> // size - параметр-выражение
class StaticArray
{
private:
	// Этот параметр-выражение управляет размером массива
	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 - параметр-выражение
class StaticArray<double, size>
{
private:
	// Этот параметр-выражение управляет размером массива
	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()
{
	// объявляем массив int для 6 чисел
	StaticArray<int, 6> intArray{};
 
	// Заполняем по порядку, затем распечатываем его
	for (int count{ 0 }; count < 6; ++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 5
4.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00

Хотя это работает, это не лучшее решение, потому что нам пришлось продублировать много кода из StaticArray<T, size> в StaticArray<double, size>.

Если бы только был способ повторно использовать код в StaticArray<T, size> в StaticArray<double, size>. Похоже, это работа для наследования!

Вы можете начать с попытки написать этот код так:

template <int size> // size - параметр-выражение
class StaticArray<double, size>: public StaticArray< // Что теперь?

Как мы можем ссылаться на StaticArray? Мы не можем.

К счастью, есть обходной путь, используя общий базовый класс:

#include<iostream>
 
template <typename T, int size> // size - параметр-выражение
class StaticArray_Base
{
protected:
	// Этот параметр-выражение управляет размером массива
	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';
	}
 
	virtual ~StaticArray_Base() = default;
};
 
template <typename T, int size> // size - параметр-выражение
class StaticArray: public StaticArray_Base<T, size>
{
public:
};
 
template <int size> // size - параметр-выражение
class StaticArray<double, size>: public StaticArray_Base<double, size>
{
public:
 
	void print()
	{
		for (int i{ 0 }; i < size; ++i)
			std::cout << std::scientific << this->m_array[i] << ' ';
            // обратите внимание: префикс this-> в строке выше необходим.
        std :: cout << '\ n';
    }
};
 
int main()
{
	// объявляем массив int для 6 чисел
	StaticArray<int, 6> intArray{};
 
	// Заполняем по порядку, затем распечатываем его
	for (int count{ 0 }; count < 6; ++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;

}

Эта программа печатает то же самое, что и выше, но имеет значительно меньше дублированного кода.

Теги

C++ / CppLearnCppДля начинающихОбучениеПрограммированиеСпециализация шаблонаШаблон / TemplateШаблон классаШаблон функции

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.