Implementing Aspects using Generative Programming

Calum Grant
 

Download files


In this article we will look at how to implement aspect-oriented programming (AOP) in C++.  AOP gives the ability to adapt existing classes or methods.  Functions often deal with many fundamentally separate things, like locking, logging, validation or error handling, which are fundamentally distinct concerns.  AOP lets you extract those concerns and implement them separately.  Each concern can be implemented separately in an "aspect" and applied to many different classes or functions without repeating the same code several times.  It makes code more reusable because aspects can be reused, and makes it easier to modify existing code.

Aspect-oriented languages such as AspectJ let you specify precisely when and where additional code should execute.  Such a specification is called a "pointcut".  A "join-point" is any point in code where stuff can be inserted, for example to insert members into a class, or to execute code before or after another function.  Inserted code is often called "advice".

Implementing all of this in standard C++ would clearly be impossible, which is why projects such as AspectC++ provide extensions to enable proper AOP in C++.  However we can write C++ in an aspect-oriented style, using template meta-programming to assemble aspects into executable code.  Template metaprogramming provides an efficient solution with no run-time overheads.

 

Such techniques fall into the category of "generative programming", which is where code is customised for you by the compiler based on your specification.

Pointcuts as template arguments

Here is a simple class we wish to customize:
class counter1
{
    int count;
public:
    counter1() : count(0) { }
    void inc(int n=1) { count += n; }
    void reset() { count=0; }
    int get() const { return count; }
};
Like other aspect-oriented systems, we want to insert behaviour before and after functions, into base classes, and be able to address individual functions.  Our first attempt is simply to supply the additional advice in template arguments.  The template arguments are effectively the pointcuts (the specification points).

In standard C++ the advice won't just insert itself and we need some helpers (base_joinpoint_list, joinpoint_list, joinpoint_list_r) to run the advice.
template<
    typename Bases,
    typename Cons,
    typename Inc,
    typename Reset,
    typename Get>
class counter2 : public base_joinpoint_list<Bases>
{
public:
    counter2()
    {
        joinpoint_list<Cons>(*this);
    }

    void inc(int n=1)
    {
        joinpoint_list<Inc>(*this, n);
    }

    void reset()
    {
        joinpoint_list<Reset>(*this);
    }

    int get() const
    {
        return joinpoint_list_r<Get>(*this);
    }
};
Here are some aspects we could supply to our counter2 class:
// Define the "core aspects" of our counter.
struct counter_base_advice
{
    int count;
    counter_base_advice() : count(0) { }
};

struct counter_inc_advice : public advice_base<>
{
    void operator()(counter_base_advice&c,int n)
    {
        c.count += n;
    }
};

struct counter_reset_advice : public advice_base<>
{
    void operator()(counter_base_advice &c)
    {
        c.count=0;
    }
};

struct counter_get_advice : public advice_base<int>
{
    int operator()(const counter_base_advice&c)
    {
        return c.count;
    }
};

Composing aspects in typelists

The power of aspects is that we can apply any number of aspects simultaneously.  We want to be able to combine (say) an aspect to validate the counter with a locking aspect or a logging aspect.  We can use "typelists" to supply a list of classes to the pointcut, which specifies all of the aspects.

A typelist is simply a data-type which encodes a list of types.  There are several implementations available, for example Boost MPL or Loki.  We can write our own just as easily:
struct empty;

template<typename H, typename T=empty>
struct list;

typedef list<A, list<B, list<C> > > ABC;
We can then supply a list of advice to our counter as follows:
struct logging_advice : public advice_base<>
{
    void onBefore(const counter_base_advice &c)
    {
        std::cout << "Entry count is " << c.count << std::endl;
    }

    void onAfter(const counter_base_advice &c)
    {
        std::cout << "Exit count is " << c.count << std::endl;
    }
};

typedef counter2<
    list<counter_base_advice>,
    list<logging_advice>,
    list<logging_advice, list<counter_inc_advice> >,
    list<logging_advice, list<counter_reset_advice> >,
    list<logging_advice, list<counter_get_advice> > >
        logging_counter2;
The functions joinpoint_list, and joinpoint_list_r, actually iterate through the type-list, executing the advice in the list.

Combining advice can do a lot more than just executing all advice in sequence.  The joinpoint_list function can make advice conditional, or execute advice around other advice.  Outer advice can catch exceptions thrown by inner advice.

If all advice inherits from advice_base, then default behaviour is supplied to our advice.

The template base_joinpoint_list inherits from every member in a type-list  (This is an exercise for the reader).

Named pointcuts

The problem with template arguments is that we need to know in advance which aspects to insert into a class.  This may not be a problem, since we can typedef different versions of the counter, and we know exactly which combination of aspects to put in our class.  However, it makes it harder to add new aspects, and it is more laborious to specify each template argument individually.  Aspects should be independent.

Ideally we would wish to write something like
struct counter_pointcut;
add_to_typelist(counter_pointcut, validating_advice);   // In validating.h
add_to_typelist(counter_pointcut, locking_advice);      // In locking.h
add_to_typelist(counter_pointcut, logging_advice);      // In logging.h
We can achieve that if we are prepared to number each item in the typelist.  We can write a macro which declares a list item in a particular position, and even declare sublists.
template<typename ListName, int Item>
struct member_of
{
    typedef void type;
};

template<typename ListName, int Item>
struct sublist_of
{
    typedef void type;
};

#define ADD_TO_LIST(List, Item, Slot) \
    template<> struct member_of<List, Slot> { typedef Item type; };

#define DEFINE_SUBLIST(List, SubList, Slot) \
    template<> struct sublist_of<List, Slot> { typedef SubList type; };

#define ATTACH_ADVICE(PointCut, Advice, Priority) \
    ADD_TO_LIST(PointCut, Advice, Priority)

We can then write a meta-program to iterate through member_of and sublist_of and assemble a typelist from our named list.  (This is an exercise for the reader.  Create the list in ascending order.  Next, traverse sublists to any depth and add them to your original list.  Finally, merge sublists such that elements in the output list appear in ascending order of their slot numbers. [Advanced - solution at http://calumgrant.net/aspects])

The slot number can be used to position our advice in the type-list.  For example we may wish our locking advice to be run before the validating advice, and the validating advice to be performed before and after the increment advice.  Using DECLARE_SUBLIST, we can also cascade pointcuts, for example adding advice to one pointcut may implicitly add it to several others.

Named type-lists allow us to attach any advice to any pointcut, and then classes can execute code in those pointcuts.
// Define some pointcuts
struct constructor_pointcut;
struct inc_pointcut;
struct reset_pointcut;
struct counter_base_pointcut;
struct counter_get_pointcut;
struct all_counter_pointcuts;

// Define relations between pointcuts.
// Here we say that all_counter_pointcuts contains the other pointcuts.
DEFINE_SUBLIST(inc_pointcut, all_counter_pointcuts, 0);
DEFINE_SUBLIST(reset_pointcut, all_counter_pointcuts, 0);
DEFINE_SUBLIST(constructor_pointcut, all_counter_pointcuts, 0);
DEFINE_SUBLIST(counter_get_pointcut, all_counter_pointcuts, 0);

ATTACH_ADVICE(counter_base_pointcut, locking_advice::base_class, 1)
ATTACH_ADVICE(all_counter_pointcuts, locking_advice , 0)

class counter3 : public base_joinpoint<counter_base_pointcut>
{
public:
    counter3()
    {
        joinpoint<constructor_pointcut>(*this);
    }

    void inc(int n=1)
    {
        joinpoint<inc_pointcut>(*this, n);
    }

    void reset()
    {
        joinpoint<reset_pointcut>(*this);
    }

    int get() const
    {
        return joinpoint_r<counter_get_pointcut>(*this);
    }
};

Summary

Aspect-oriented programming allows different concerns to be separated into aspects, which can then be applied to many different functions or classes.  In standard C++ we can achieve this in a number of ways.  In this article we looked at using typelists to specify the aspects in a pointcut.  Using generative programming techniques, we can do various things such as executing a list of advice, inheriting from a typelist or assembling a typelist from a pointcut name.  Template metaprogramming has the benefit of giving a solution with no run-time overheads.

Further reading

Aspect orientation and C++,

 

  1. R.E.Filman, T.Elrad, S.Clarke and M.Aksit (Eds) "Aspect-oriented software development" Addison-Wesley 2005.
  2. C.Diggins. "Aspect-Oriented Programming & C++" Dr Dobbs Journal, August 2004.
  3. AspectC++ http://www.aspectc.org/
 

Typelists and template metaprogramming,

 

  1. K.Czarnecki and U.W.Eisenecker, "Generative Programming: Methods, Tools, and Applications", Addison-Wesley 2000.
  2. A.Gurtovoy and D. Abrahams, "The Boost MPL Library" http://www.boost.org/libs/mpl/doc/index.html.
  3. A.Alexandrescu, "Modern C++ Design", Addison-Wesley 2001.
  4. A.Alexandrescu, "The Loki Library" http://loki-lib.sourceforge.net.