Сообщений 15 Оценка 581 Оценить |
Время от времени при работе с шаблонами возникает необходимость специализировать шаблон класса по одному из аргументов. В качестве примера можно рассмотреть шаблон классов матриц, параметризованный типом элемента и размерами матрицы.
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. |
Задачу построения подобной метафункции решили в 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 необходим для того, чтобы классы, имеющие операцию приведения к указателю, не считались указателями. На первый взгляд может показаться, что можно «упростить» дискриминирующие функции ptr_discriminator, избавившись от промежуточного класса PointerShim:
TrueType simple_ptr_discriminator(const volatile void*); FalseType simple_ptr_discriminator(...); |
Однако, в этом случае, метафункция IsPointer будет работать неверно, например, для таких классов:
struct C { operator int*() const { return 0; } }; |
Так как класс C имеет операцию приведения к указателю, функция simple_ptr_discriminator может быть вызвана с любым объектом этого класса, и, следовательно, метафункция, построенная с использованием simple_ptr_discriminator, будет ошибочно определять подобные классы как указатели.
Пример. Для пущей ясности можно рассмотреть, как работает метафункция IsPointer<T> на примере типа int. IsPointer<int> разворачивается компилятором примерно в следующее:
// псевдокод class IsPointer<int> { private: static int t_; public: enum { value = sizeof(ptr_discriminator(t_)) == sizeof(TrueType) }; }; |
ptr_discriminator(PointerShim) для t_ не подходит, т.к. объект PointerShim может быть создан только из указателя. Следовательно, подходящей будет оставшаяся ptr_discriminator(...), которая возвращает FalseType. Значит, в данном случае выражение sizeof(ptr_discriminator(t_)) эквивалентно выражению sizeof(FalseType), значение которого по условию не равно sizeof(TrueType). Следовательно, IsPointer<int>::value == false.
Использовать полученную метафункцию IsPointer<T> для симуляции частичной специализации по виду аргумента шаблона можно примерно следующим образом:
// Реализация общего случая: T не является указателем. template<class T> class C_ { // . . . }; // Реализация случая, когда T является указателем. template<class T> class C_ptr_ { // . . . }; // Traits для случая, когда T является указателем template<bool T_is_ptr> struct CTraits { template<class T> struct Args { typedef C_ptr_<T> Base; }; }; // Traits для случая, когда T не является указателем. template<> struct CTraits<false> { template<class T> struct Args { typedef C_<T> Base; }; }; // Класс, предназначенный для использования клиентами. template<class T> class C : public CTraits<IsPointer<T>::value>::template Args<T>::Base { // . . . }; |
Приведенная техника симуляции частичной специализации обладает некоторыми ограничениями по сравнению с «настоящей» частичной специализацией шаблонов классов.
Одним из наиболее заметных ограничений является то, что дискриминирующие функции, применяющиеся при создании многих метафункций, требуют объявления переменной, поэтому не работают с абстрактными классами. Например, в случае с IsPointer<T> объявляется статическая переменная t_. Несмотря на то, что ее определение не требуется, специализация шаблона IsPointer<T> абстрактным классом приведет к ошибке компиляции. По этой же причине приходится предоставлять специализации шаблонов метафункций для void.
Другим ограничением является то, что некоторые метафункции, построенные с использованием дискриминирующих функций, например, IsConst<T>, IsVolatile<T>, IsReference<T> и т.п., некорректно работают в случае, если T имеет квалификаторы и const и volatile одновременно (например, const volatile int&). Существующая реализация метафункций IsConst<T> и IsVolatile<T> без «настоящей» частичной специализации сводится к использованию соответствующих дискриминирующих функций:
TrueType const_discriminator(const volatile void*); FalseType const_discriminator(volatile void*); template<class T> struct IsConst { private: static T t_; public: enum { value = sizeof(const_discriminator(&t_)) == sizeof(TrueType) }; }; template<> class IsConst<void> { public: enum { value = false }; }; TrueType volatile_discriminator(const volatile void*); FalseType volatile_discriminator(const void*); template<class T> struct IsVolatile { private: static T t_; public: enum { value = sizeof(volatile_discriminator(&t_)) == sizeof(TrueType) }; }; template<> class IsVolatile<void> { public: enum { value = false }; }; |
Очевидно, что эти метафункции не работают, если в качестве аргумента им передан тип имеющий как const, так и volatile квалификацию. Реализация IsReference<T> основывается на том факте, что добавление cv-квалификации к ссылке игнорируется:
template<class T> class IsReference { private: typedef T const volatile cv_t_; public: enum { value = !IsConst<cv_t_>::value || !IsVolatile<cv_t_>::value }; }; template<> class IsReference<void> { public: enum { value = false }; }; |
Так как метафункция IsReference<T> использует метафункции IsConst<T> и IsVolatile<T>, естественно, что она имеет те же недостатки.
ПРИМЕЧАНИЕ Описание и анализ других полезных метафункций, основанных на дискриминирующих функциях, выходит за рамки данной статьи и оставляется в качестве упражнения читателю. Например, можно построить метафункцию IsDerived<T, Base>, позволяющую специализировать шаблоны для наследников определенного класса. |
Еще одним достаточно важным ограничением техник симуляции частичной специализации является то, что еще никому не удавалось (и вряд ли удастся), например, получить тип T, имея T&. С использованием «настоящей» частичной специализации эта задача решается тривиально:
template<class T> struct RemoveReference { typedef T Type; }; template<class T> struct RemoveReference<T&> { typedef T Type; }; |
Описанная техника позволяет использовать преимущества частичной специализации шаблонов классов даже в случае отсутствия соответствующей поддержки со стороны компилятора. Комбинация приведенной методики с метафункциями при необходимости позволяет описывать достаточно сложные условия специализации шаблонов.
Единственным «серьезным» требованием к компилятору является наличие реализации шаблонов членов классов. Симуляция частичной специализации была проверена на следующих компиляторах:
Хотя последние четыре и поддерживают частичную специализацию, иногда может быть полезным прибегать к технике симуляции в случае одновременного использования нескольких компиляторов, один из которых «не дорос» до частичной специализации. При этом удобно, если использование условной компиляции можно минимизировать.
Сообщений 15 Оценка 581 Оценить |