2022-07-02

The ABCs of aspect oriented C++ (part1)

OK, so the title is a little ambitious, but I am going to show you how to prove a limited level of aspect orientations for a few cases.

What the heck is "aspect oriented programming", anyway?

Honestly, I'm not strong on the subject never have worked in that format myself, but the short version is that it allows you to add behavior to program abstraction without editing the associated code. Common examples are attaching entry and exit loggers or call count tracker to functions without editing the function. C++ has no built in support for it.

Anyway, I'm going to start by showing you a very limited and specific trick that has the same flavor, then generalize it.

Example: interface with non-trivial behavior

We have a hand-rolled serialization mechanism we use on a work project. It supports selected fundamental and standard-library type and provides an interface (AKA Abstract Base Class) for building support into classes we write for ourselves1

class SerialInterface { public: SerialInterface1() = default; ~SerialInterface1() = default; virtual bool serialize(std::ostream & out) const = 0; virtual bool deserialize(std::istream & in) = 0; };

Class we want to (de)serialize derive from SerialInterface and implement the virtual methods using some utility code to maintain uniform behavior.2 The return values tell you if the process was successful or not.

We have classes whose sole purposes is to be serialized and deserialized to support interprocess data transfer, which makes it very convenient to accept the input stream in a constructor

class Client: public SerialInterface1 { public: Client(std::istream & serialData) : Client() { deserialize(serialData); } };

But what happened to the status? It's lost, which is a problem.

Now, we could throw in the the event deserialize returns failure, but who like exceptions? An alternative would be to cache the success value and offer an interface to query the value. But then you have to duplicate the code in every client class. What we would like is to give the base class the status caching behavior without needing any duplicated code in the client classes. We achieve this by splitting the implementation of serialize and deserialize into a wrapper defined in the base class and a code implementation defined by the clients.

class SerialInterface2 { public: SerialInterface2() = default; ~SerialInterface2() = default; bool status() const { return _serializeImpl; } bool serialize(std::ostream & out) const { _serializationStatus = serializeImpl(out); return status(); } bool deserialize(std::istream & in) { _serializationStatus = deserializeImpl(out); return status(); } private: virtual bool serializeImpl(std::ostream & out) const = 0; virtual bool deserializeImpl(std::istream & in) = 0; bool _serializationStatus = true; };

Now all client class support status inquires without needing any code to support it. We've provided a status aspect to all the clients.

More general aspects for callables

It is very common for the examples I see in explanations of AOP to focus on providing aspects to callable subprograms (functions, procedures, whatever you want to call them). And that is interesting because modern C++ already has a highly general idea of "a thing you can call like a function", which means that we can try to use the approach from SerialInterface2 to provide aspects to callables.

To generalize the behavior we provide virtual implementations for code to be run both before and after the the callable.

class FunctionAspect { public: using function_t = bool(int, double&, const std::string &); FunctionAspect() = delete; FunctionAspect(std::function & func): _callable(func) {} bool operator()(int i, double & d, const std::string & s); private: virtual void prologue() = 0; virtual void epilogue() = 0; std::function _callable; }; bool FunctionAspect::operator()(int i, double & d, const std::string & s) { prologue(); bool result = _callable.operator()(i,d,s); epilogue(); return result; }

Concrete derivative classes can then provide arbitrary code wrappers around any std::function with the appropriate signature. That restriction is, of course, a major drawback, but we're on the road to general purpose aspects.


1 Come to think of it, we could have avoided exactly this complexity if we had access to a general support for AOP in C++ in the first place.

2 Yeah, we should probably have used JSON, but it works.

No comments:

Post a Comment