|
Классика жанра: книга одного из основоположников современного подхода к метапрограммированию в языке C++ |
Удивительная по простоте реализации и необычности концепция в C++. Списки объектов - это привычно и понятно. Но в языке C++ тип - это сущность, которая имеет только внешние проявления (лексическое - в виде спецификации типа long int или std::list) и внутреннее - специфичное для каждого компилятора и извлекаемое на свет божий только опосредованно с помощью оператора typeid. В среде .Net все проще - типы (их спецификация) с помощью определенных манипуляций могут становиться объектами System.Type, в результате чего задача манипулирования типами сводится к известной задаче манипулирования объектами. В стандартном C++ все гораздо интереснее, так как стандарт не предусматривает, к примеру, возможность создания объекта, тип которого задан строкой. То есть манипулировать типами вроде как и невозможно?
Возможно. Более того, типы можно собирать в списки, а затем творить с этими списками чудеса - получать тип по индексу, искать заданный тип в списке, добавлять типы в список, удалять элементы из списка, наконец - сортировать списки. Конечно, существует простой способ создавать объекты для типа, помещенного в список.
Заинтригованы? Поглядываете недоверчиво на текст?
Главное заклинание - шаблоны. С их помощью удается подняться на метауровень языка C++ для магических манипуляций с типами. Причем заклинания для списков типов работают на этапе компиляции программы - как и все, что связано с шаблонами.
Самое простое и очевидное применение - проверка принадлежности типа к сформированному подмножеству (аналогичная функциональность может быть найдена в библиотеке Boost.Type_traits). В предлагаемом Вашему вниманию простом примере (см. далее) показано применение нескольких техник метапрограммирования в C++, в том числе - выбор кода на стадии компиляции (подробнее об этом здесь), проверка условия на стадии компиляции, чтобы не допускать вычисления корня для указателей (подробнее об этом здесь), ну и конечно - работа со списком типов.
Андрей Александреску в своей книге "Modern C++ Design: Generic Programming and Design Patterns applied". Сами классы для работы со списками типов находятся в библиотеке Loki.
Сам список типов устроен настолько просто, что на первый взгляд даже не верится. Но это - простота колеса, которое применяется тысячью способов.
template < class T, class U >
struct
{
typedef T Head;
typedef U Tail;
};
Head - это голова списка (единственный тип), а Tail - его хвост, который может быть и списком типов, и нулевым типом.
Нулевой тип (null type) - это класс, объявленный в библиотеке Loki так:
class NullType {};
Нельзя создать объект этого класса (так как публичного конструктора у него нет), и единственное, для чего он нужен - служить индикатором конца списка типов.
Таким образом, список с одним типом char создается так:
typedef TypeList<char,NullType> OneTypeList;
Для списка из двух типов char и int, вопреки вашим ожиданиям, нужен такой синтаксис:
typedef TypeList<char, TypeList<int,NullType> > TwoTypes;
Обратите внимание, что хотя задавать второй параметр шаблона TypeList надо либо как список типов, либо как NullType, но если Вы зададите его как обычный тип - вроде ничего и не произойдет. Но некоторые метафункции работы со списками типов работать откажутся - с выдачей невнятной диагностики. Чтобы явно задать проверку на соответствие параметров шаблонов заданным критериям, необходимо привлекать средства библиотеки Boost.Concept_check, но это мы пока оставим без реализации.
Создавать длинные списки непосредственно рекурсивным конструированием крайне неудобно. Такой стиль создания списков характерен для языка Лисп, но он нынче не в фаворе. В библиотеке Loki предусмотрен набор довольно удобных макросов, позволяющих создавать списки натуральным образом:
typedef TYPELIST_1(int) list1;
typedef TYPELIST_2(int,long) list2;
Определение макросов достаточно тривиально (см. библиотечный хидер loki/typelist.h:
#define TYPELIST_1(T1) TypeList< T1, NullType >
#define TYPELIST_2(T1,T2) TypeList< T1, TYPELIST_1(T2) >
#define TYPELIST_3(T1,T2,T3) TypeList< T1, TYPELIST_2(T2,T3) >
и так далее. Библиотека Loki содержит макросы для создания списков типов вплоть до 50 элементов, хотя при необходимости легко добавить новые макросы.
Библиотека Loki содержит реализацию следующих функций для списков типов:
Length<> - вычисление длины списка
TypeAt<> - доступ к элементам списка по индексу
IndexOf<> - поиск индекса типа в списке
Append<> - добавление элемента к списку
Erase<> - удаление элемента из списка
NoDuplicates<> - удаление повторов типов из списка
MostDerived<> - частичная сортировка списка (производные классы переносятся в начало списка)
Рассмотрим детально самую простую функцию для списков типов - вычисление длины списка. Вместо цикла используется рекурсия (это обычное дело в обобщенном программировании на C++ - см. к примеру статью о вычислении факториала). Критерием выхода из рекурсии служит то факт, что последний элемент списка - это обязательно NullType.
template <class TList> struct Length;
//
Для NullType длина списка равна
0
template <> struct Length<NullType>
{
enum { value=0 };
};
//
Рекурсивная формула
template <class T,class U>
struct Length< TypeList< T, U> >
{
enum { value = 1 + Length<U>::value };
};
Использовать метафункцию для определения длины списка можно примерно так:
typedef TYPELIST_2(int,long) list2;
const int n2 = Loki::TL::Length<list2>::value;
В этой простой программе показано, как можно реализовать два алгоритма - пусть это будет вычисление квадратного корня - для типов, которые могут принимать отрицательные значения, и для остальных типов (то есть беззнаковых). Вторая реализация будет немного более эффективна, так как в ней не будет проверки аргумента на неотрицательность.
Скачать проект для MS VisualStudio 2003
Прежде всего, чтобы комфортно изучать операции со списками типов, желательно как-то распечатывать их. Вы должны понимать, что все операции со списками типов выполняются на стадии компиляции, к примеру - для доступа к любому элементу надо указать его индекс как константу времени компиляции (а не переменную цикла). Поэтому просто написать функцию, которая в цикле for вызывает Loki::TL::TypeAt<> для каждого элемента не получится. Но ничто не мешает нам написать простой метаалгоритм - с помощью двух рекурсивно вызываемых шаблонных функций можно печатать весь список:
// Вспомогательная функция - печатает
символическое представление типа T
template < typename T >
void Print_Type(void) { cout << "(" << typeid( T ).name() << ") "; }
// **********************************************************************
// Две шаблонные функции образуют ЦИКЛ по всем элементам списка типов.
//
// 1------
template < typename TList >
void Print_Type_List( TList x )
{
Print_Type< TList::Head >();
Print_Type_List( TList::Tail() );
}
// 2------
template <>
void Print_Type_List<NullType>( NullType )
{
}
Вызов функции Print_Type<> с явным указанием списка как параметра специализации выполняет печать:
typedef
TYPELIST_4(int,double,long,char*) list1;
Print_Type_List<list1>(list1());
Результат (скриншот)

Волшебную работу по преобразованию типа в строку описывающих его лексем выполняет стандартный C++ оператор typeid.
Предложенное решение не лишено недостатка (можете подумать сами - чтобы напечатать список типов, надо создать объект этого списка и передать его как аргумент функции Print_Type_List), но вполне работоспособно для экспериментов.
Этот список составляется очень просто:
// Список типов, которые могут
хранить отрицательные значения.
typedef TYPELIST_7(signed char,int,long,float,double,long double,__int64)
signeds;
Определенный в библиотеке Loki макрос TYPELIST_7 делает всю утомительную работу. После этого со списком signeds можно начинать работать.
Вообще говоря, технология подробно описана в другой статье "Выбор кода при компиляции". Вкратце идея такова.
Во-первых создает шаблонный класс, содержащий статический метод для выполнения вычисления:
// Класс, содержащий статический
метод для вычисления корня для знаковых типов
template < typename T, bool is_signed >
struct sqrt_resolver
{
static T calc( T arg )
{
// Проверяем (во время компиляции, что тип T не является указателем).
STATIC_CHECK( Loki::TypeTraits<T>::isPointer==false,
Pointers_Arent_Allowed );
T res=arg/2;
// ...
return res;
}
};
И специализацию для значения параметра is_signed=false:
// Случай для знаковых типов - надо
проверять знак.
template < typename T >
struct sqrt_resolver< T, true >
{
static T calc( T arg )
{
if( arg<0 )
throw std::exception();
T res=arg/2;
// ...
return res;
}
};
Теперь сама функция, которая вызывается пользовательским кодом и направляет компиляцию в одну из двух альтернатив:
template < typename X >
X square_root( X x )
{
return sqrt_resolver<X,Loki::TL::IndexOf<signeds,X>::value!=-1>::calc(x);
}
Как видите, принадлежность типа аргумента функции к подмножеству определяется с помощью метода Loki::TL::IndexOf<>. Слово 'метод' написано курсовом, так как на самом деле это - шаблонный класс, хотя функционально и синтаксически все выглядит как вызов метода.
Внутри одной из двух специализаций класса-вычислителя можно увидеть проверку типа - не указатель ли это (опять-таки с помощью средств библиотеки Loki, хотя в Boost.Type_traits есть аналогичные средства). В этом случае мы вплотную приближаемся к такому интересному вопросу, как concept check - проверка правильности использования шаблона (см. библиотеку Boost.Concept_check).
создано 12.06.2005
последние изменения 20.06.2005
© Mental Computing 2010