Павел Кузнецов Симуляция частичной специализации
Введение
Время от времени при работе с шаблонами возникает необходимость специализировать шаблон класса по одному из аргументов. В качестве примера можно рассмотреть шаблон классов матриц, параметризованный типом элемента и размерами матрицы. template‹class T, int Rows, int Columns› class Matrix { //… }; Предположим, в процессе разработки выяснилось, что производительность программы неудовлетворительна, и узким местом является функция умножения матриц с элементами типа float, и что эту проблему можно решить путем использования intrinsic-функций процессора. При наличии соответствующей поддержки компилятора это легко можно сделать при помощи так называемой частичной специализации шаблонов классов: template‹int Rows, int Columns› class Matrix‹float, Rows, Columns› { //… }; Однако некоторые компиляторы не поддерживают частичную специализацию, и, как следствие, «не понимают» подобные конструкции. Желание получить эквивалентную функциональность при работе с такими компиляторами приводит к технике, описанной ниже.Техника симуляции
Естественным первым шагом будет вынести различающуюся функциональность Matrix‹› в два базовых класса: Matrix_‹›, реализующий общий случай, и Matrix_float_‹› для специфики Matrix‹float,…›. template‹class T, int Rows, int Columns› class Matrix_ { //… };template‹int Rows, int Columns› class Matrix_float_ { //… }; Таким образом, проблема сведется к тому, чтобы класс Matrix‹T, Rows, Columns› наследовался от Matrix_‹T, Rows, Columns› или Matrix_float_‹Rows, Columns›, в зависимости от того, является ли параметр T шаблона Matrix‹› типом float. Решение этой задачи и является главным «фокусом» данной техники. Несмотря на отсутствие поддержки частичной специализации, компилятор позволяет специализировать шаблоны полностью. Этот факт можно использовать для построения вложенных шаблонов с полной специализацией и выбором подходящего базового класса на соответствующем уровне вложенности. template‹class T› struct MatrixTraits { template‹int Rows, int Columns› struct Dimensions { typedef Matrix_‹T, Rows, Columns› Base; }; };
template‹› struct MatrixTraits‹float› { template‹int Rows, int Columns› struct Dimensions { typedef Matrix_float_‹Rows, Columns› Base; }; }; Теперь осталось просто унаследовать Matrix‹› от соответствующего класса MatrixTraits‹›::…::Base. template‹class T, int Rows, int Columns› class Matrix: public MatrixTraits‹T›::template Dimensions‹Rows, Columns›::Base { //… }; ПРИМЕЧАНИЕ Согласно текущей версии стандарта, использование ключевого слова template при квалификации вложенного шаблона Dimensions в данном случае обязательно, хотя некоторые компиляторы и позволяют его опускать.
Использование
Метапрограммирование и метафункции
Прежде чем перейти к изложению дальнейшего материала, полезно ввести понятия метапрограммирования и метафункции. Если внимательнее посмотреть на то, что происходит, когда компилятор встречает пример, подобный наследованию класса Matrix от MatrixTraits‹T›::…::Base, можно заметить, что фактически это является программированием компилятора. То есть, в данном случае компилятор как бы получает инструкцию: «если тип шаблона является типом float, то считать базовым классом Matrix_float_‹›, в противном случае – Matrix_‹›. Это можно рассматривать как программирование вычислений времени компиляции. Подобные техники иногда называют метапрограммированием шаблонами или просто метапрограммированием, а шаблоны, подобные MatrixTraits, – метафункциями.Частичная специализация по виду аргумента шаблона
Одним из аспектов частичной специализации является возможность специализировать шаблон по виду аргумента, например, предоставить общую для всех указателей специализацию шаблона: template‹class T› class С { //… };template‹class T› class С‹T*› { //… }; Применительно к описанной технике, проблему можно свести к задаче создания метафункции, определяющей, является ли данный тип указателем: template‹class T› struct IsPointer { static const bool value =…; }; где IsPointer‹T›::value принимает значения true или false в зависимости от того, является ли тип T указателем. ПРИМЕЧАНИЕ Так как некоторые компиляторы не поддерживают должным образом определение статических констант времени компиляции в теле класса, эта метафункция может быть переписана эквивалентным образом с использованием enum.
Метафункция IsPointer‹T›
Задачу построения подобной метафункции решили в 2000 году сотрудники Adobe Systems Incorporated Мэт Маркус и Джесс Джонс. Суть решения сводится к использованию выражения вызова перегруженных функций внутри sizeof(): // Типы TrueType и FalseType могут быть определены произвольным образом, // главное чтобы выполнялось условие: sizeof(TrueType)!= sizeof(FalseType). struct TrueType {char dummy_ [1];}; struct FalseType {char dummy_ [100];};// Промежуточный класс PointerShim нужен, // чтобы избежать ошибочной работы метафункции // IsPointer в случае параметризации классом, в котором определен // оператор преобразования к указателю. struct PointerShim { PointerShim(const volatile void*); }; // Т.к. функции ptr_discriminator на самом деле не вызываются, реализации не требуется. TrueType ptr_discriminator(PointerShim); FalseType ptr_discriminator(…);
// IsPointer‹T›::value == true, если T является указателем, // IsPointer‹T›::value == false в противном случае. template‹class T› class IsPointer { private: static T t_; public: enum { value = sizeof(ptr_discriminator(t_)) == sizeof(TrueType)}; };
// Так как объект типа void создан быть не может, // случай IsPointer‹void› должен обрабатываться отдельно. template‹› class IsPointer‹void› { public: enum {value = false}; }; ПРЕДУПРЕЖДЕНИЕ Строго говоря, необходимо предоставлять не только специализацию для void, но и для соответствующих cv-квалифицированных разновидностей: const void, volatile void, const volatile void. Эти специализации опущены для краткости изложения. ПРИМЕЧАНИЕ Функции, подобные ptr_discriminator, иногда называют дискриминирующими. Техника основана на том, что во время компиляции выражения sizeof(ptr_discriminator(t_)) компилятор вынужден выбрать из двух перегруженных функций ptr_discriminator наиболее подходящую. В случае, если IsPointer‹T›::t_ является указателем, будет выбрана функция ptr_discriminator(PointerShim), возвращающая значение типа TrueType, и значение IsPointer‹T›::value обращается в true, т.к. sizeof(ptr_discriminator(PointerShim)) – sizeof(TrueType); в противном случае подходящей является функция ptr_discriminator(…)и значением IsPointer‹T›::value является false, т.к. sizeof(ptr_discriminator(…)) – sizeof(FalseType), а типы TrueType и FalseType выбраны таким образом, что sizeof(TrueType)!= sizeof(FalseType). Класс PointerShim необходим для того, чтобы классы, имеющие операцию приведения к