Что такое специализация шаблона частичная специализация шаблона
Шаблоны (C++)
Шаблоны служат основанием для универсального программирования на C++. В качестве строго типизированного языка C++ требует, чтобы все переменные имели конкретный тип, либо явно объявленный программистом, либо выведенный компилятором. Однако многие структуры данных и алгоритмы выглядят одинаково независимо от типа, на котором они работают. Шаблоны позволяют определить операции класса или функции и предоставить пользователю указание конкретных типов, с которыми должны работать эти операции.
Определение и использование шаблонов
Шаблон — это конструкция, которая создает обычный тип или функцию во время компиляции на основе аргументов, предоставленных пользователем для параметров шаблона. Например, можно определить шаблон функции следующим образом:
В других случаях пользователь может объявить экземпляр шаблона, специализированный для типа int. Предположим, что get_a () и get_b () — это функции, возвращающие int:
Однако, поскольку это шаблон функции, и компилятор может вывести тип T из аргументов T и b, можно вызвать его так же, как обычная функция:
Когда компилятор встречает последнюю инструкцию, он создает новую функцию, в которой каждое вхождение T в шаблоне заменяется на :
Правила, определяющие, как компилятор выполняет выведение типов в шаблонах функций, основаны на правилах для обычных функций. Дополнительные сведения см. в разделе разрешение перегрузки вызовов шаблона функции.
Параметры типа
В minimum приведенном выше шаблоне Обратите внимание на то, что параметр типа minimum не определен каким-либо образом, пока он не будет использован в параметрах вызова функции, где добавляются квалификаторы Const и Reference.
Практически не существует ограничения на количество параметров типа. Несколько параметров разделяются запятыми:
Ключевое слово class эквивалентно typename в данном контексте. Предыдущий пример можно выразить следующим образом:
Оператор с многоточием (. ) можно использовать для определения шаблона, принимающего произвольное число из нуля или более параметров типа:
Будет создана ошибка компилятора, так как не MyClass предоставляет перегрузку для . Обратите внимание, что аргументы должны быть указателями
Параметры, не являющиеся типами
В отличие от универсальных типов на других языках, таких как C# и Java, шаблоны C++ поддерживают Параметры, не являющиеся типами, также называемые параметрами значений. Например, можно предоставить постоянное целочисленное значение, чтобы указать длину массива, как в этом примере, подобно классу std:: Array в стандартной библиотеке:
Обратите внимание на синтаксис в объявлении шаблона. size_t Значение передается в качестве аргумента шаблона во время компиляции и должно быть const или constexpr выражением. Используйте его следующим образом:
Другие виды значений, включая указатели и ссылки, могут передаваться как параметры, не являющиеся типами. Например, можно передать указатель на функцию или объект функции для настройки некоторой операции внутри кода шаблона.
Выведение типа для параметров шаблона, не являющихся типами
в Visual Studio 2017 и более поздних версиях, а /std:c++17 также в режиме или более поздней версии компилятор выводит тип аргумента шаблона, не являющегося типом, который объявлен с помощью auto :
Шаблоны в качестве параметров шаблона
Шаблон может быть параметром шаблона. В этом примере MyClass2 имеет два параметра шаблона: параметр typeName T и параметр шаблона arr:
Аргументы шаблона по умолчанию
Шаблоны классов и функций могут иметь аргументы по умолчанию. Если шаблон имеет аргумент по умолчанию, его можно оставить неопределенным при его использовании. Например, шаблон std:: Vector имеет аргумент по умолчанию для распределителя:
В большинстве случаев класс по умолчанию std:: распределитель приемлем, поэтому вы используете такой же вектор:
Но при необходимости можно указать пользовательский распределитель следующим образом:
При наличии нескольких аргументов шаблона все аргументы после первого аргумента по умолчанию должны иметь аргументы по умолчанию.
При использовании шаблона, параметры которого заданы по умолчанию, используйте пустые угловые скобки:
Специализация шаблонов
В некоторых случаях невозможно или нежелательно, чтобы шаблон определял точно такой же код для любого типа. Например, может потребоваться определить путь кода, который будет выполняться, только если аргумент типа является указателем или std:: wstring или типом, производным от конкретного базового класса. В таких случаях можно определить специализацию шаблона для этого конкретного типа. Когда пользователь создает экземпляр шаблона с этим типом, компилятор использует специализацию для создания класса, а для всех остальных типов компилятор выбирает более общий шаблон. Специализации, в которых все параметры являются специализированными, являются полными специализациями. Если только некоторые из параметров являются специализированными, это называется частичной специализацией.
Шаблон может иметь любое количество специализаций, если каждый специализированный параметр типа уникален. Только шаблоны классов могут быть частично специализированными. Все полные и частичные специализации шаблона должны быть объявлены в том же пространстве имен, что и исходный шаблон.
Дополнительные сведения см. в разделе специализация шаблонов.
Урок №179. Частичная специализация шаблона
Обновл. 15 Сен 2021 |
На этом уроке мы рассмотрим, что такое частичная специализация шаблона в языке С++, как она используется и какие есть нюансы.
Проблема
На уроке №176 мы узнали, каким образом можно использовать дополнительный параметр шаблона. Рассмотрим еще раз класс StaticArray из материалов того же урока:
Здесь у нас есть 2 параметра шаблона класса: параметр типа и параметр non-type.
Теперь предположим, что нам нужно написать функцию для вывода всех элементов массива. Хотя мы можем сделать это через метод класса, мы реализуем это через отдельную функцию (ради лучшего погружения в тему).
Используя шаблон функции, мы можем написать следующее:
Это позволит нам сделать:
Хотя всё работает правильно, но есть один нюанс. Рассмотрим следующий код функции main():
Примечание: Мы рассматривали strcpy_s на уроке о строках C-style.
Программа скомпилируется со следующим результатом:
Для всех типов, кроме char, имеет смысл помещать пробел между каждым элементом массива, чтобы элементы не «слипались». Однако с типом char есть смысл вывести всё вместе, как строку C-style, чтобы не было лишних пробелов. Как мы можем это исправить?
Полная специализация шаблона — решение?
Сначала мы могли бы подумать об использовании специализации шаблона функции. Однако проблема с полной специализацией шаблона заключается в том, что все параметры шаблона должны быть явно определены. Например:
Как вы можете видеть, мы добавили шаблон функции print() для работы с типом char. Результат:
Хотя одна проблема решена, возникает другая проблема: использование полной специализации шаблона класса означает, что мы должны явно указывать длину передаваемого массива! Рассмотрим следующий пример:
Очевидно, что полная специализация шаблона класса здесь является решением-костылем. Частичная специализация шаблона — вот, что нам нужно.
Частичная специализация шаблона
Частичная специализация шаблона позволяет выполнить специализацию шаблона класса (но не функции!), где некоторые (но не все) параметры шаблона явно определены. Для нашей вышеприведенной задачи идеальное решение заключается в том, чтобы шаблон функции print() работал со StaticArray типа char, но при этом размер массива не являлся фиксированным значением, а мог варьироваться.
Вот наш шаблон функции print(), который принимает частично специализированный шаблон класса StaticArray:
19.3 – Специализация шаблона функции
При создании экземпляра шаблона функции для заданного типа компилятор создает копию шаблонной функции и заменяет шаблонные параметры типа фактическими типами, используемыми в объявлении переменной. Это означает, что каждая конкретная функция будет иметь такие же детали реализации, что и другие созданные экземпляры шаблона функции (только с использованием разных типов). Хотя в большинстве случаев это именно то, что вам нужно, иногда бывают случаи, когда полезно реализовать шаблонную функцию, немного отличающуюся для определенного типа данных.
Один из способов добиться этого – специализация шаблона.
Давайте посмотрим на очень простой шаблон класса:
Приведенный выше код отлично подходит для многих типов данных:
template<> сообщает компилятору, что это шаблон функции, но у которой нет никаких шаблонных параметров (поскольку в этом случае мы явно указываем все типы). Некоторые компиляторы могут позволить вам опустить эту запись, но с ней будет правильнее.
В результате, когда мы повторно запустим приведенную выше программу, она напечатает:
Еще один пример
Теперь давайте рассмотрим еще один пример, в котором может быть полезна специализация шаблонов. Подумайте, что произойдет, если мы попытаемся использовать наш шаблонный класс Storage с типом данных const char* :
Как оказалось, вместо того, чтобы печатать имя, второй вызов функции storage.print() ничего не печатает! Что тут происходит?
Теперь, когда переменные типа Storage выходят за пределы области видимости, память, выделенная в специализированном конструкторе, будет удалена в специализированном деструкторе.
Хотя в приведенных выше примерах используются только функции-члены, вы можете точно так же специализировать шаблоны функций, не являющихся членами.
Трюки со специализацией шаблонов C++
Специализация шаблонов является одной из «сложных» фичей языка с++ и использутся в основном при создании библиотек. К сожалению, некоторые особенности специализации шаблонов не очень хорошо раскрыты в популярных книгах по этому языку. Более того, даже 53 страницы официального ISO стандарта языка, посвященные шаблонам, описывают интересные детали сумбурно, оставляя многое на «догадайтесь сами — это же очевидно». Под катом я постарался ясно изложить базовые принципы специализации шаблонов и показать как эти принципы можно использовать в построении магических заклинаний.
Hello World
Как мы привыкли использовать шаблоны? Используем ключевое слово template, затем в угловых скобках имена параметров шаблона, после чего тип и имя. Для параметров также указывают что это такое: тип (typename) или значение (например, int). Тип самого шаблона может быть класс (class), структура (struct — вообщем-то тоже класс) или функция (bool foo() и так далее). Например, простейший шаблонный класс ‘A’ можно задать вот так:
Через некоторое время мы захотим, чтобы наш класс для всех типов работал одинаково, а для какого-нибудь хитрого вроде int — по-другому. Фигня вопрос, пишем специализацию: выглядит так же как объявление но параметры шаблона в угловых скобках не указываем, вместо этого указываем конкретные аргументы шаблона после его имени:
Готово, можно писать методы и поля специальной реализации для int. Такая специализация обычно называется полной (full specialization или explicit specialization). Для большинства практических задач большего не требуется. А если требуется, то…
Специализированный шаблон — это новый шаблон
Специализированный шаблон может иметь свои параметры шаблона
Дьявол, как известно, в деталях. То, что специализированный шаблонный класс это совсем-совсем новый и отдельный класс конечно интересно, но магии в этом мало. А магия есть в незначительном следствии — если это отдельный шаблонный класс, то он может иметь отдельные, никак не связанные с неспециализированным шаблонным классом параметры (параметры — это то, что после template в угловых скобках). Например, вот так:
Правда, именно такой код компилятор не скомпилирует — новые параметры шаблона S и U мы никак не используем, что для специализированного шаблонного класса запрещено (а то что это класс специализированный компилятор понимает потому, что у него такое же имя ‘A’ как у уже объявленного шаблонного класса). Компилятор даже специальную ошибку скажет: «explicit specialization is using partial specialization syntax, use template<> instead». Намекает, что если сказать нечего — то надо использовать template<> и не выпендриваться. Тогда для чего же в специализированном шаблонном классе можно использовать новые параметры? Ответ странный — для того, чтобы задать аргументы специализации (аргументы — это то, что после имени класса в угловых скобках). То есть специализируя шаблонный класс мы можем вместо простого и понятного int специализировать его через новые параметры:
Такая странная запись скомпилируется. И при использовании получившегося шаблонного класса с std::map будет использована специализация, где тип ключа std::map будет доступен как параметр нового шаблона S, а тип значения std::map как U.
Такая специализация шаблона, при которой задается новый список параметров и через эти параметры задаются аргументы для специализации называется частичной специализацией (partial specialization). Почему «частичной»? Видимо потому, что изначально задумывалась как синтаксис для специализации шаблона не по всем аргументам. Пример, где шаблонный класс с двумя параметрами специализируется только по одному из них (специализация будет работать когда первый аргумент, T, будет указан как int. При этом второй аргумент может быть любым — для этого в частичной специализации введен новый параметр U и указан в списке аргументов для специализации):
Магические последствия частичной специализации
Из двух вышеописанных свойств специализации шаблонов есть ряд интересных следствий. Например, при использовании частичной специализации можно, вводя новые параметры шаблона и описывая через них специализированные аргументы, разбивать составные типы на простейшие. В приведенном ниже примере специализированный шаблонный класс A будет использован, если аргументов шаблона является тип указателя на функцию. При этом через новые параметры шаблона S и U можно получить тип возвращаемого значения этой функции и тип ее аргумента:
А если в специализированном шаблоне объявить typedef или static const int (пользуясь тем, что это новый шаблон), то можно использовать его для извлечения нужной информации из типа. Например, мы используем шаблонный класс для хранения объектов и хотим получить размер переданного объекта или 0, если это указатель. В две строчки:
Магия этого типа используется в основном в библиотеках: stl, boost, loki и так далее. Конечно, при высокоуровневом программировании использовать такие фокусы череповато — думаю, все помнят конструкцию для получения размера массива :). Но в библиотеках частичная специализация позволяет относительно просто реализовывать делегаты, события, сложные контейнеры и прочие иногда очень нужные и полезные вещи.
Коллеги, если найдете ошибку (а я, к сожалению, не гуру — могу ошибаться) или у Вас есть критика, вопросы али дополнения к вышеизложенному — буду рад комментариям.
19.5 – Частичная специализация шаблона
Этот и следующий урок не обязательны для прочтения и предназначены для тех, кто хочет получить более глубокие знания о шаблонах в C++. Частичная специализация шаблонов используется не так часто (но в определенных случаях может быть полезна).
В уроке «19.2 – Шаблонные параметры, не являющиеся типами данных» вы узнали, как можно использовать параметры-выражения для параметризации шаблонов классов.
Этот класс принимает два параметра шаблона, параметр-тип и параметр-выражение.
Теперь предположим, что мы хотели бы написать функцию для печати всего массива. Хотя мы могли бы реализовать это как функцию-член, вместо этого мы собираемся сделать это как функцию, не являющуюся членом, потому что это упростит последующие примеры.
Используя шаблоны, мы могли бы написать что-то вроде этого:
Это позволит нам сделать следующее:
и получаем следующий результат:
Хотя это работает, у этого решения есть недостаток в дизайне. Рассмотрим следующий код:
(Если вам нужно освежить память, то std::strcpy мы рассмотрели в уроке «10.6 – Строки в стиле C».)
Эта программа скомпилируется, выполнится и выдаст следующее значение (или подобное):
Для типов, не являющихся символами, имеет смысл вставлять пробел между элементами массива, чтобы они не шли вместе. Однако с типом char может быть лучше печатать всё вместе как строку в стиле C, чего наша функция print() не делает.
Итак, как мы можем это исправить?
Специализация шаблонов приходит на помощь?
Сначала можно подумать об использовании специализации шаблонов. Проблема с полной специализацией шаблона заключается в том, что все параметры шаблона должны быть определены явно.
Очевидно, что полная специализация шаблона здесь является слишком ограничивающим решением. Решение, которое мы ищем, – это частичная специализация шаблона.
Частичная специализация шаблона
Вот наш пример с перегруженной функцией печати, которая принимает частично специализированный StaticArray :
Вот полная программа, использующая этот шаблон:
Эта программа печатает:
Частичная специализация шаблона может использоваться только с классами, но не с шаблонами функций (функции должны быть полностью специализированными). Наш пример void print(StaticArray &array) работает, потому что функция печати не является частично специализированной (это просто перегруженная функция, использующая частично специализированный параметр типа класса).
Частичная специализация шаблона для функций-членов
Ограничение частичной специализации функций может привести к некоторым проблемам при работе с функциями-членами. Например, что, если бы мы определили StaticArray так?
К сожалению, это не работает, потому что мы пытаемся частично специализировать функцию, что запрещено.
Итак, как нам это обойти? Один из очевидных способов – частично специализировать весь класс:
Эта программа печатает:
Вы можете начать с попытки написать этот код так:
К счастью, есть обходной путь, используя общий базовый класс:
Эта программа печатает то же самое, что и выше, но имеет значительно меньше дублированного кода.