Аспектно-ориентированное программирование (АОП) – это парадигма
программирования, которая делает возможным прозрачно разбить программу на «аспекты»,
включая соответсвующее разделение, композицию и повторное использование кода
аспекта. АОП определяет процесс соединение аспектов в одно целое.
Вместе с тем, часто возникают ситуации, когда базовый класс должен иметь
информацию о своих потомках, например для безопасного приведения типов.
Статический полимофизм в С++ (иначе изместный как Curiously recurring template pattern или CRTP) – это идиома, в которой класс Х наследуется от
класса-шаблона, который инстанцируется своим наследником X.
Таким образом, базовый класс может обладать знанием об отнаследованном
типе. И АОП, и CRTP широко
применяются в различных ситуациях в С++. На практике, существуют достаточно
простые реализация АОП, использующая шаблоны. Однако в случае совместного
использования АОП и CRTP
возникают сложности. Хотя и существует диалект языка C++, который называется AspectC++, мы не будем его рассматривать, так как
он требует своего расширения компилятора и поэтому его не является стандартным
C++. Ниже мы рассмотрим простое решение проблемы на стандартном C++ без каких либо накладных расходов.
Проблема совместного использования АОП и CRTP
В некоторых случаях было бы очень здорово совместно использовать АОП и CRTP, однако, как мы увидим ниже, при совместном их использовании возникают некоторые
сложности.
На примере в листинге 1 показана подбная попытка, добавление
функциональности с помощью аспектов в базовый класс с названием Number.
Мы можем наблюдать, что возвращаемый тип операторов + и - структуры ArithmeticAspect должен знать о полном типе FULLTYPE при попытке расширить функцональность базового
класса с помощью перегрузки оператора. Мы обратимся к этой проблеме в далее.
//basic class
class Number
{
protected:
UnderlyingType n;
};
//aspects
template <class
NextAspect>
struct ArithmeticAspect:
public NextAspect
{
FULLTYPE operator+
(const FULLTYPE& other) const;
// What type is FULLTYPE?
FULLTYPE operator-
(const FULLTYPE& other) const;
FULLTYPE& operator+=
(const FULLTYPE& other);
FULLTYPE& operator-=
(const FULLTYPE& other);
};
template <class
NextAspect>
struct LogicalAspect
: public NextAspect
{
bool operator! () const;
bool operator&& (const FULLTYPE&
other) const;
bool operator|| (const FULLTYPE& other)
const;
};
//decorating Number
with aspectual code
typedef LogicalAspect
<ArithmeticAspect<Number >
> MyIntegralType;
Листинг 1
Минимальное решение
Базовый принцип этого решения не отличается по своей сути от традиционного
решения, упомянутого выше.
Проблема
Объект Number занимает место последнего аспекта в
списке аспектов. Однако, он должен обладать информацией о списке аспектов
(поскольку является шаблонным аргументом шаблона) которому он сам принадлежит.
Это приводит к извечной проблеме «курицы и яйца».
Например, если бы Number обладал
информацией о требуемом типе, он мог бы использовать ее для определения
возвращаемого типа для своих операторов, как это показано на Листинге 2. Этот
листинг демонстрирует конструкцию CRTP с одним аспектом, которая работает замечательно.
LogicalAspect<ArithmeticAspect<Number<??>>>
template <template
<class> class Aspects>
class Number
{
typedef Aspects<Number<Aspects>>
FullType;
...
};
ArithmeticAspect<Number<ArithmeticAspect>>
Листинг 2
С другой стороны,
это приводит к проблеме использование еще одного аспекта, поскольку ему
требуется шаблонный агрумент шаблона, который невозможно вывести, как это
показано в коде выше.
У нас есть два решения: первое – очень простое, оно основано на
использовании новой кострукции С++11, называемой альясом шаблона, второе – с
использованием шаблонов с переменным количеством парамтров (иначе известных как
variadic templates, новшество С++11), но несмотря на то, что они
доступны в С++11, их можно просто реализовать и в С++98. Оба приема используют
идиомы, описанные ниже, главной целью которых является использование в виде
библиотек, обеспечивая дружественный синтаксис и возможность повторного
использования кода.
Предлагаемая идиома языка
Одним из
возможных решений могло бы быть применение вручную подготовленного альяса
шаблона, как показано ниже.
//with template alias:
template <class T>
using LogicalArithmeticAspect =
LogicalAspect<ArithmeticAspect<T>>;
//without template alias:
template <class T>
struct LogicalArithmeticAspect
{
typedef
LogicalAspect<ArithmeticAspect<T>>
Type;
};
and with minor
changes in the Number base class’s code, we could write
the following
declaration:
LogicalArithmeticAspect
<
Number<LogicalArithmeticAspect>
>
Хотя это и решает
проблему, данный вариант решения непрактичен, и кроме того увеличит количество
кода, при увеличении требуемых комбинаций, что приведет к необходимости
копи-паста.
Мы попытаемся
найти более общее решение для рассматриваемого вопроса, которое позволит
изебежать избыточного копирования кода и позволит оформить решение в виде
отдельной библиотеки.
template
<template <template <class>
class> class Base>
class Decorate
{
public:
template<template <class>
class ... Aspects>
struct with
{
//...
};
//...
private:
struct Apply
{ …
};
};
Листинг 3
Итак, в порядке обеспечения понятного решения и реализации в виде
библиотеки, мы будем использовать новые возможности языка – шаблоны с
переменным числом параметров, и тем самым мы сможем прозрачно выразить наши
намерения: «декорировать» базовый класс списком аспкетов. Пример того, что мы собираемся
реализовать представлен ниже:
Decorate<Number>::with<ArithmeticAspect,
LogicalAspect>
Прообраз класса Decorate представлен в
листинге 3, детали реализации будут отличаться в зависимости от применяемого
решения.
В обоих решениях, вложенный класс, который определяется выражением with, и внутренний вспомогательный класс Apply имеют различную реализацию.
Решение 1: использование альяса шаблона из С++11
В данном решении, примерная реализация Decorate::with
представлена на листинге 4, а структура вспомогательного объекта Apply, который также использует альясы шаблона на
листинге 5.
Несмотря на различия реализаций, цель одна и та же и основная идея будет
описана ниже при рассмотрении второго решения.
template<template
<class> class ... Aspects>
struct with
{
template <class T>
using AspectsCombination =
typename Apply<Aspects...>::template
Type<T>;
typedef
AspectsCombination
<Base<AspectsCombination>>
Type;
};
Листинг 4
template<template
<class> class A1,
template <class> class
... Aspects>
struct Apply<A1,
Aspects...>
{
template <class T>
using Type = A1
<typename Apply
<Aspects...>::template
Type<T>>;
};
Листинг 5
Решение 2: без использование шаблонного альяса
В данном решении, примерная реализация Decorate::with
представлена на листинге 6
Комбинирование аспектов
Теперь, когда мы имеем список аспектов, как мы можем их комбинировать.
Решение которое мы предлагаем – создать класс Binder, как показано на листинге 7.
Binder
инкапсулирет аспект (как шаблонный аргумент шаблона) с полным типом Binder<Aspect>. Дополниельно, он делает возможным
привязывать последующий аспект или базовый класс, через доступ к внутреннему
классу Binding.
Способ, с помощью которого Binder
позволяет создавать конечный тип продемонстрирован ниже.
Binder<ArithmeticAspect,
Binder<LogicalAspect>>::Binding<Number>::Type
Давайте проанализируем шаг за шагом этот листинг (начиная с внутреннего
объявления):
1.
Binder <LogicalAspect> использует
второе определение, оно просто обеспечивает полный тип для аспекта с
возможностью привязки к другому полному типу.
2.
Binder<ArithmeticAspect, Binder<LogicalAspect>> использует первое определение.
Привязка генерирует Binder к ArithmeticAspect и привязывает его к Binder<LogicalAspect>::Binding<T> генерируя
шаблонный аргумент шаблона в который входят оба аспекта.
template<template
<class> class ... Aspects>
struct with
{
typedef typename Apply<Aspects...>::Type
TypeP;
typedef typename TypeP::template Binding
<
Base<TypeP::template Binding>
>::Type Type;
};
Листинг 6
struct None
{
};
template <template
<class> class A,
class B = None>
struct Binder
{
template <class T>
struct Binding
{
typedef
typename Binder<A>::template
Binding
<
typename B::template
Binding<T>::Type
>::Type Type;
};
};
template<template
<class> class T>
struct Binder<T,
None>
{
template <class P>
struct Binding
{
typedef T<P> Type;
};
};
Листинг 7
(Вкраце, Binder генерирует шаблонный аргумент шаблона
– соединяя аспекты Arithmetic с Logical – для использования в базовом классе)
Наконец, тип встраивается в базовый класс Number. Поскольку данная реализация не совсем очевидна, мы предоставили
упрощенную версию в листинге 8 для иллюстрации сути.
template <template
<class> class A,
class B = None>
struct Binder
{
template <class T>
struct Binding
{
typedef
Binder<A>::Binding
<
B::Binding<T>::Type
>::Type Type;
};
};
Листинг 8
template <template
<class> class ... Aspects>
struct Apply;
template <template <class>
class T>
struct Apply<T>
{
typedef Binder<T> Type;
};
template<template <class>
class A1,
template <class> class ...
Aspects>
struct Apply<A1, Aspects...>
{
typedef Binder<A1, typename Apply
<
Aspects...
>::Type> Type;
};
template<template <class>
class ... Aspects>
struct with
{
typedef
typename Apply<Aspects...>::Type
TypeP;
typedef
typename TypeP::template Binding
<
Base<TypeP::template Binding>
>::Type
Type;
};
Листинг 9
Теперь у нас есть Binder, с помощью
которого можно комбинировать аспекты и затем встраивать их в базовый класс.
Применение списка аспектов в классе Number.
Все что нам осталось сделать, это применить наш класс Bind к списку аспектов. Для того что бы это
реализовать, мы определим вспомогательную структуру Apply, которая рекурсивно применяет класс Binder для каждого аспекта, как показано на листинге 9.
Мы используем эту вспомогательную структуру для того, что бы сгенерировать
конечный аспект, как композицию всех необходимых, и встроить его в базовый
класс. После этого ::Then
содержит именно то что нам нужно, вот и все.
Использование библиотеки.
Библиотека, реализующая эту идиому обеспечивает два инсрумента: получение FullType и построение его.
Листинг 10 показывает способ получение FullTypee с Aspect. Давайте взглянем на финальный пример,
использующий два аспекта представленных ранее:
typedef Decorate<Number>::with<ArithmeticAspect,
LogicalAspect>::Type
ArithmeticLogicalNumber;
Обратите внимание, что оба решения представленые ранее имеют одинаковый
интерфейс, поэтому данный пример
применим к каждому из них.
//basic class
template
<template <class> class
Aspects>
class Number
{
typedef Aspects<Number<Aspects>>
FullType;
//...
};
// aspect example
template <class
NextAspect>
struct
ArithmeticAspect: public NextAspect
{
typedef typename NextAspect::FullType FullType;
FullType
operator+ (const FullType& other)
const;
// ...
};
Листинг 10
Альтернатива С++98 и полный код
Подобная идея может быть реализована с помощью списков типов (typelists) из предыдущего стандарта, С++98.
Полный код библиотеки и примеров как для С++11 так и для С++98 доступен по адресу
http://cpp-aop.googlecode.com
Напоследок
Мы считаем что решение проблемы затронутой в данной публикации могло бы
быть более лакончиным, если бы в языке было бы ключевое слово для получения
полного типа. Мы предлагаем взять это предложение на рассмотрение для
последующих ревизий стандарта языка.
By HUGO ARREGUI, CARLOS CASTRO AND DANIEL GUTSON, Overload
109
Комментариев нет:
Отправить комментарий