Stanislav Arnaudov

Generic Execution Around Pointer

· [Stanislav Arnaudov] · 3 minute read · 729 words

Abstract

I recently found out what the Execute-Around_Pointer idiom in C++ is. What it does is track access to a specific object. For example, when you want to observe how certain properties of an object change on each method call, you would employ the use of this idiom. Think of it like wrapping each method call for an object with additional function calls. This, for empale, is ugly code:

std::vector<int> vec{1,2,3,4};

std::cout << std::size(vec)
vec.push_back(5);
std::cout << std::size(vec)

std::cout << std::size(vec)
vec.push_back(6);
std::cout << std::size(vec)

With the Execute-Around_Pointer you can “prettify” this pattern by constructing a special “tracer” object that internally keeps a reference to the vector. The underlying object is then accessed through the -> operator. The code then can become:

std::vector<int> vec{1,2,3,4};
VectorSizeTracer vec_tracer{vec};
vec_tracer->push_back(5);
vec_tracer->push_back(6);

On the wiki page there is a similar example. The implementation there is, however, non-genric and does not use any templated code. I decided to write several classes that realize the logic of the idiom. Those can be then used to quickly construct different tracers for different types of types.

Making it generic

Iterating over tuple

First off, we’ll need one utility method. Namely, for_each for iterating over a tuple. Weirdly enough, there is no such method in the standard library. It’s not that hard to implement something ourselves though:

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline std::enable_if_t<I == sizeof...(Tp), void>
for_each(std::tuple<Tp...> &, FuncT)
{ }

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline std::enable_if_t<I < sizeof...(Tp), void>
for_each(std::tuple<Tp...>& t, FuncT && f)
{
    f(std::get<I>(t));
    for_each<I + 1, FuncT, Tp...>(t, std::forward<FuncT>(f));
};

Special thanks to this StackOverflow answer. The function just recursively loops over the tuple by keeping track of the current index in the I template parameter. The recursion is terminated when I is equal to the size of the tuple. If you’ve programmed in Haskel, this should not be too hard for you.

The proxy class

Next, we need that class that can keep a pointer to an object as well as several other objects that will be notified when the pointer is accessed.

template<typename T, typename...Tracker>
struct proxy
{
public:

    proxy(T& obj, Tracker && ... track) :
        m_obj(&obj),
        m_track(std::forward<decltype(track)>(track)...)
    {
        (track.before(obj), ...);
    }

    ~proxy()
    {
        detail::for_each(m_track, [&](auto& tracker){tracker.after(*m_obj);});
    }

    T* operator ->() { return m_obj;}
private:
    T* m_obj;
    std::tuple<Tracker ...> m_track;
};

The pointer is stored in the m_obj member. The notified objects are stored in the m_track tuple. Those can be of different types but must implement a before and a after method. The before methods of each object will be called before any operation on the object pointed by m_obj is performed, and the after method – after. We call the after method in the destructor by using our for_each function that we previously implemented. In the constructor, I’ve simply used pack expansion.

The Tracer

Now we have every that we need to write the final tracer class:

template<typename T, typename ...Tracker>
class Tracked : public Tracker ...
{
  public :
    explicit Tracked(T& obj) : m_obj(obj)
    {}

    detail::proxy<T, Tracker...> operator ->() {
        return detail::proxy<T, Tracker...>(m_obj, static_cast<Tracker>(*this)...);
    }

  private :
    T& m_obj;

};

On construction, the Tracked class takes the object that needs to be traced as well as several types – the Tracker variadic template argument. The Tracker class will be derived from each of the passed in types. This means that it will “have” all of the methods defined by the Tracker types. In the overload of the -> operator, we create a proxy object with the underlying object and statically cast version of the this object with each of the given Tracker types. This is possible because we’ve derived the Tracked from each of those types.

Usage

With our defined classes, we can realize the logic of the Execute-Around-Pointer idiom in an easy manner. For example, if we want to track the size of a vector we can write something like:

// class with before and after methods
class SizeTracker
{
    public:
    static void before(std::vector<int>& vec) {
        std::cout << "Size Before:" << std::size(vec) << "\n";
    }

    static void after(std::vector<int>& vec) {
        std::cout << "Size After:" << std::size(vec) << "\n";
    }
};

// tracker type for vector of ints.
using VectorSizeTracker = Tracked<std::vector<int>, SizeTracker>;

With this, we can rewrite the example that I gave at the beginning of the post.

int main()
{
    std::vector<int> vec{1,2,3,4};
    VectorSizeTracker vec_track{vec};

    vec_track->push_back(5);
    vec_track->push_back(6);

    return 0;
}