Метапрограммирование на C++: выбор кода при компиляции (compile-time switch)

Определение проблемы

Наша задача - написать метакод, который будет работать аналогично оператору switch - то есть выбирать для компиляции участок в зависимости от значения целочисленной константы. В книге [1] соответствующий раздел озаглавлен "Отображение целочисленных констант на типы (mapping integral constants to types)".

Допустим, мы пишем некий класс-шаблон, который обрабатывает обобщенные символьные данные - тип символов задается как параметр шаблона и может быть произвольным составным.

template < typename T >
class Char_Processor ...

Один из участков кода выполняет копирование списка обобщенных символов - как это обычно делает C-функция strcpy или стандартный алгоритм std::copy. Так как обычно функция побитового копирования memcpy работает очень быстро, то мы хотим для типов T, допускающих побитовое копирование, использовать memcpy, а для остальных - std::copy.

Таким образом, задача имеет две грани: 1) выяснить, допускает ли тип T побитовое копирование, 2) выбрать в зависимости от этого оптимальную процедуру копирования.

Можно конечно задать вопрос: а чем не устраивает оператор if? Написать что-то типа:

if( is_pod )
 memcpy( ... );
else
 std::copy( ... );

и успокоиться, не наводить метатень на плетень? Но в общем случае такой подход невозможен, так как компилятор обязательно должен оттранслировать обе ветки оператора if, даже если условие известно ему при компиляции. Если мы пишем обобщенный алгоритм, то может оказаться, что одна из веток при некотором наборе параметров шаблона, где спрятан if, просто будет синтаксически некорректной и компилятор откажется работать, хотя в наши намерения входило использовать лишь одну ветку if, которая синтаксически корректна.

Решение проблемы - отображение целочисленных констант на типы Loki::Int2Type

Можно (а в ряде случаев лучше всего) решать эту задачу с использованием средств препроцессора - директивами #if ... #elif ... #else ... #endif. Но препроцессор не видит C++-кода и, значит, не сможет помочь в случаях, когда необходимо выбрать алгоритм (функцию или класс) на основании неких атрибутов классов. В нашем случае препроцессор ни коим образом не может выяснить, является ли тип T так называемым POD (plain old data - простой структурой в стиле C), допускающей побитовое копирование, и выбрать для конкретного случая подходящий код.

Определить, допускает ли T побитовое копирование, в общем случае невозможно. Библиотека BOOST C++ имеет соответствующие средства в подбиблиотеке Boost.Type_traits - включив хидер <boost/type_traits.hpp>, можно использовать шаблон boost::is_pod<T>, который во время компиляции выясняет необходимый факт и в зависимости от этого инициализирует поле value. К огромному сожалению, работа boost::is_pod<T> основана на некоторых нестандартных (пока) средствах языка C++, и поэтому на многих популярных платформах не поддерживается.

Поэтому решение, является ли тип POD, оставим программисту - так в нашем классе обработки символов появляется новый аргумент типа bool (так называемый policy):

template < typename T, bool Is_Pod > ...

Продолжим написание метаалгоритма.

Обычным способом выбора функции в практике программирования на C++ является перегрузка функций - одно из фундаментальных свойств языка (кстати перегрузка функций, function overloading - это тоже выбор кода на этапе компиляции). В этом случае компилятор выбирает один из альтернативных вызовов функции на основании имеющихся типов аргументов. Однако в нашем случае использование перегрузки невозможно - выбор должен быть выполнен в зависимости от значения аргумента, а не его типа.

Как поступают в таких случаях математики - сведем задачу к известной. Попробуем все-таки воспользоваться перегрузкой функций для выбора алгоритма. Нам нужно сделать маленький шаг - научится отображать целочисленную константу в тип. Другими словами, для разных целочисленных аргументов нужно получать разные типы - которые приводили бы к выбору разных перегруженных функций.

Вот простой шаблон, который делает черную работу:

template < int v >
struct Int2Type
{
 enum { value=v };
};

Классика жанра: книга одного из основоположников современного подхода к метапрограммированию в языке C++

Современное проектирование на С++: Обобщенное программирование и прикладные шаблоны проектирования: подробнее

Андрей Александреску
Современное проектирование на С++: Обобщенное программирование и прикладные шаблоны проектирования

Для разных параметров шаблона v получаются разные типы. Они имеют одинаковое имя, но для удобства имя конкретизации шаблона приводят вместе с параметрами, например Int2Type<0>, Int2Type<100>. Этот шаблон объявлен в библиотеке Loki, краткое описание которой можно найти на нашем сайте, а более полное - в книге Александреску.

Теперь осталось воспользоваться перегрузкой функций:

template < typename T, bool Is_Pod >
class Char_Processor
{
 ...
 inline void Copy( T *src, int n, T *dst,  Int2Type<true> ) const { memcpy( dst, src, sizeof(src)*n ); }
 inline void Copy( T *src, int n, T *dst, Int2Type<false> ) const { std::copy( src, src+n, dst ); }
 ...
 inline Process( T *src, int n, T *dst )
 {
  ...
  Copy( src, n, dst, Int2Type<Is_Pod>() );
 }
};

В примере, проект и исходные тексты которого Вы можете скачать по ссылке внизу страницы, можно в режиме пошаговой отладки увидеть, что компилятор на самом деле на стадии трансляции программы выбирает один из вызовов функции - на основании целочисленного флага (bool). Таким образом, поставленная задача - написать метааналог оператора switch - полностью решена.

Похожий механизм реализован для базового типа контейнеров библиотеки LEM. Если Вы взгляните на файл lem_carr.h, где объявлены классы Collect<T> (контейнер-вектор общего назначения) и MCollect<T> (контейнер-вектор для POD, допускающих побитовое копирование), то увидите, что оба этих шаблонных класса получаются специализацией единственного базового шаблона, которому передается признак 'тип - POD'. На основании этого признака выбирается один из алгоритмов копирования элементов (Copy_Factory_COMMON или Copy_Factory_POD).

 

Скачать исходные тексты проекта (2.2 Кб)

 

Метапрограммирование в BOOST: выбор подходящего типа на этапе компиляции

Библиотека Boost C++ содержит множество инструментов, которые позволяют реализовать условный выбор кода на стадии компиляции. К примеру, Boost.Integer позволяет выбирать подходящий целочисленный тип на основе некоторых характеристик - минимального числа битов, к примеру.

Еще один практический пример

Любой C++ программист быстро усваивает технологию перегрузки функций в зависимости от типов аргументов. Мы предлагаем Вам посмотреть, как перегрузка функций выполняется в зависимости от того, может ли аргумент быть отрицательным (если нет - применяется более эффективная процедура вычисления без проверки - это все для вычисления квадратного корня) - см. статью "Списки типов".

 

Литература

1. Часть материала данной статьи написана на основе и по мотивам главы 2 книги "Modern C++ Design: Generic Programming and Design Patterns applied" by Andei Alexandrescu.

последние изменения 18.06.2005

  © Mental Computing 2010