Показаны сообщения с ярлыком шаблонное метапрограммирование. Показать все сообщения
Показаны сообщения с ярлыком шаблонное метапрограммирование. Показать все сообщения

суббота, 21 июня 2014 г.

Шаблонное метапрограммирование в C++: генерация списков с произвольными полями

Я приведу псевдокод, который отразит смысл задачи.
list  Person1;

Person1.add_record( Name: "Paul" );
Person1.add_record( Age: 45 );
Список Person1 из этого примера можно реализовать с помощью структуры с полями Name и Age. Но в следующем определении мы, возможно, захотим изменить поля списка и нам понадобится новая структура. Как обойтись без явного определения структур-контейнеров и использовать одно обобщенное определение? Слово обобщенный (generic) содержит ответ — нужно привлечь к решению этой задачи шаблоны C++. Нам нужны линейные комбинации полей списков. Публичное наследование в C++ — отличный инструмент для создания линейных комбинаций! Я приведу код, добавляющий новую запись в список, а затем объясню его смысл. Имя файла — generator.hh. Здесь и далее стражи включения пропущены ради лаконичности.
#define DECLARE_RECORD( Name, Type, Value ) \
    template < typename  Parent >           \
    struct  Rec##Name : public Parent       \
    {                                       \
        Rec##Name() : Name( Value ) {}      \
        Type  Name;                         \
    };                                      \
    template <>                             \
    struct  Rec##Name< Generator::End >     \
    {                                       \
        Rec##Name() : Name( Value ) {}      \
        Type  Name;                         \
    };

namespace  Generator
{
    typedef int  End;
}
Макрос DECLARE_RECORD объявляет шаблоны структур с именем Rec##Name, которые содержат одну единственную запись, соответствующую одной из записей в формируемом списке: он реализует функцию add_record() из приведенного выше псевдокода. Структура может наследовать типу Parent — такой же структуре с другой единственной записью, либо замыкаться с помощью объявления Generator::End. Тип этого объявления может быть достаточно произвольным, я выбрал int. Формирование списка должно представлять собой создание списка типов из типов, объявленных с помощью DECLARE_RECORD. Давайте создадим простую базу персональных данных: файл persons.hh.
#include "generator.hh"

#define DECLARE_PERSON( Name, Id ) \
    Persons::Id::RecName< Persons::Id::RecAge< Generator::End > >  Name;

namespace  Persons
{
    namespace  Person1
    {
        DECLARE_RECORD( Name, const char *, "Paul" )
        DECLARE_RECORD( Age, size_t, 45 )
    }

    namespace  Person2
    {
        DECLARE_RECORD( Name, const char *, "Helen" )
        DECLARE_RECORD( Age, size_t, 38 )
    }
}
Макрос DECLARE_PERSON — это и есть механизм для формирования списка с записями имени (Name) и возраста (Age) персоналий. Список формируется нанизыванием имен структур, объявленных макросами DECLARE_RECORD ниже и заканчивается объявлением Generator::End. Для различения персоналий используются пространства имен Person1 и Person2. Заметьте, что здесь нет ни объектов времени исполнения, ни объектов времени компиляции: объявления DECLARE_RECORD разворачивают объявления шаблонов на стадии препроцессорной обработки, инстанцирование же этих шаблонов будет происходить во время применения макроса DECLARE_PERSON. Давайте напишем простой тест (файл test.cc).
#include <iostream>
#include "persons.hh"

int  main( void )
{
    DECLARE_PERSON( rec1, Person1 )
    std::cout << "1. Name: " << rec1.Name << ", Age: " << rec1.Age << std::endl;

    DECLARE_PERSON( rec2, Person2 )
    std::cout << "2. Name: " << rec2.Name << ", Age: " << rec2.Age << std::endl;
}
После компиляции запустим программу на исполнение.
g++ -o test test.cc
./test
1. Name: Paul, Age: 45
2. Name: Helen, Age: 38
Работает. А теперь создадим базу данных книг в файле books.hh. В каждой записи будет три поля: название (Name), автор (Author) и год выпуска (Year).
#include "generator.hh"

#define DECLARE_BOOK( Name, Id ) \
    Books::Id::RecName< Books::Id::RecAuthor< \
                                Books::Id::RecYear< Generator::End > > >  Name;

namespace  Books
{
    namespace  Book1
    {
        DECLARE_RECORD( Name, const char *, "War and Peace" )
        DECLARE_RECORD( Author, const char *, "Lev Tolstoy" )
        DECLARE_RECORD( Year, int, 1873 )
    }
}
Макрос DECLARE_BOOK выполняет ту же роль, что и DECLARE_PERSON при создании списка персональных данных, то есть создает список данных одной книги путем нанизывания типов, объявленных ниже с помощью макросов DECLARE_RECORD, в список типов, замыкаемый объявлением Generator::End. Добавим нашу новую книгу в тестовую программу. Для этого в файле test.cc нужно включить файл books.hh
#include "books.hh"
и в конце функции main() поместить строки
    DECLARE_BOOK( rec3, Book1 )
    std::cout << "3. Name: " << rec3.Name << ", Author: " << rec3.Author <<
                 ", Year: " << rec3.Year << std::endl;
Компилируем и запускаем на исполнение.
g++ -o test test.cc
./test
1. Name: Paul, Age: 45
2. Name: Helen, Age: 38
3. Name: War and Peace, Author: Lev Tolstoy, Year: 1873

среда, 26 января 2011 г.

Простейший пример шаблонного метапрограммирования в C++

Под метапрограммированием обычно понимают предоставление специальных средств внутри или снаружи исходного кода, которые позволяют выводить часть программы автоматически. Таким спецсредством в C++ являются шаблоны (templates). Шаблоны C++ обеспечивают особый метаязык, по своим характеристикам являющийся функциональным и понимаемый исключительно компилятором C++. С функциональными языками шаблонный метаязык сближают наличие общих подходов (сопоставление с образцом, рекурсия) и запрет переменных-состояний.
Рассмотрим следующую ситуацию. Допустим мы хотим создать класс, услуги которого базируются на сходных по назначению, но отличных в их реализации классах BaseA и BaseB. Если оба базовых класса обладают схожим интерфейсом, то логично оформить наш класс в виде шаблона:
    template  < typename  Base >
    class  Derived  : public Base
    {
    };
(Я не буду заострять внимание на содержимом классов, поэтому наш класс пустой, хотя фактически он может содержать множество полей и методов.)
Подразумевается, что вместо шаблонного аргумента Base нужно будет подставлять BaseA либо BaseB в зависимости от требований разработчика. А теперь главный вопрос: что если разработчику необходима информация о базовых классах объектов, основанных на шаблоне Derived. Как ее получить?
На вскидку я могу перечислить четыре способа:
  1. Вполне возможно, что классы BaseA и BaseB сами предоставляют информацию о своих типах. Но это не всегда так, а изменять эти классы мы не можем.
  2. Использование RTTI и dynamic_cast. Во-первых, это не всегда доступно, ведь для использования dynamic_cast необходимо, чтобы BaseA и BaseB были полиморфными, т.е. содержали виртуальные функции. Во-вторых, использование RTTI чревато потерей эффективности.
Оставшиеся два способа связаны с введением в шаблон класса Derived нового метода GetBaseClass(), предоставляющего информацию о базовом типе:
    template  < typename  Base >
    class  Derived : public Base
    {
        public:
            BaseClass  GetBaseClass( void ) const;
    };
BaseClass - это enum, перечисляющий базовые классы, а также значение Prohibited:
    enum  BaseClass
    {
        Prohibited,
        BaseClassA,
        BaseClassB
    };
Итак, оставшиеся способы:
  1. Специализация шаблона Derived для отдельных типов базового класса. К сожалению, шаблон класса Derived может содержать большое число полей и методов, его полная специализация приведет к нежелательному дублированию кода.
  2. Использование трюка, основанного на шаблонном метапрограммировании.
Четвертый способ - это и есть то, что я хотел здесь показать. Он основан на введении вспомогательного шаблона класса (или структуры) BaseClassInstance, не содержащего ничего, кроме статической константы типа BaseClass, и специализированного для разных фактических типов базовых классов:
    template  < typename >
    struct  BaseClassInstance
    {
        static const BaseClass  value = Prohibited;
    };


    template  <>
    struct  BaseClassInstance< BaseA >
    {
        static const BaseClass  value = BaseClassA;
    };


    template  <>
    struct  BaseClassInstance< BaseB >
    {
        static const BaseClass  value = BaseClassB;
    };
(В первом определении после ключевого слова typename можно было указать имя аргумента, например Base, но, поскольку оно не используется в теле шаблона, я его опустил.)
Несмотря на то, что по форме здесь перечисляются разные специализации шаблона структуры C++, по сути, если рассматривать это с точки зрения функционального программирования, здесь объявлены клозы простейшей функции, возвращающей значение типа BaseClass в зависимости от результата сопоставления с образцом (типом - шаблонным аргументом)!
Нам осталось написать тело метода GetBaseClass():
    template  < typename  Base >
    BaseClass Derived< Base >::GetBaseClass( void ) const
    {
        return BaseClassInstance< Base >::value;
    }
Теперь объекты классов, основанных на шаблоне Derived, обладают информацией о своем базовом классе. Эта информация закладывается в момент инстанцирования классов, а значит нет дополнительных нагрузок в ран-тайме, и, что не менее важно, нам не пришлось дублировать исходный код.