Monday 26 January 2009

operator->() ad infinitum

Short version:

In C++, if an implementation of operator->() returns something different than a raw pointer, operator->() will be invoked on that return value; this will happen again and again until at some point a raw pointer is returned.


Lil' longer version:

One of the operators C++ allows to be overloaded is "->" . This enables syntactic constructions that mimic pointer usage, like smart pointers, which look like this...


template <typename T>
class MySmartPointer {
public:
MySmartPointer(T* pointee) : pointee_(pointee) {}

T* operator->() { return pointee_; }

/* some magic here... */

private:
T* pointee_;
};


Thus, you can use MySmartPointer with the same syntax as a plain T* :


MyClass* x (new MyClass);
x->doStuff();

MySmartPointer <MyClass> y (new MyClass);
y->doStuff();


However, if we declare operator->() to return something that is not a raw pointer, the compiler will generate code to invoke operator-> on that object, and so on:


struct Baz {
void sayHello() { cout << "hello" <<>() { return baz_; }
};
struct Bar {
Baz* baz_;
Baz* operator->() { return baz_; }
};
struct Foo {
Bar bar_;
Bar operator->() { return bar_; }
};

void doStuff () {
Foo foo;
foo->sayHello();
}



'doStuff' is equivalent to this:

void doStuff () {
Foo foo;
Bar bar (foo.operator->());
Baz* baz = bar.operator->();
baz->sayHello();
}


Bjarne Stroustrup used this language feature -plus a stack allocated auxiliary object- in a proposal for a C++ idiom for wrapping calls to member functions, enabling to perform stuff just before and just after any method call (i.e. intercept it). Let's see it in an example:


template<typename T>
class LockSmartPtrAux {
public:
LockSmartPtrAux(T* t, Lock& lock)
: t_(t), lock_(lock) {
lock_.lock(); /* lock at creation */
}
~LockSmartPtrAux() {
lock_.unlock(); /* unlock at destruction */
}
T* operator->() { return t_; }
private:
T* t_;
Lock& lock_;
};

template<typename T>
class LockSmartPtr {
public:
LockSmartPtr(T* t) : t_(t) {}
LockSmartPtrAux<T> operator->() {
return LockSmartPtrAux<T>(t_, lock_);
}
private:
T* t_;
Lock lock_;
};
// LockSmartPtrAux should be declared inside LockSmartPtr to enhance symbol locality


With the class templates above, you can "wrap" any class so that every invocation to its member functions is guarded by a lock, thus achieving some degree of transparent thread safety:


map<string, MyClass> cache;
/* fill cache with useful and hard to build stuff... */
LockSmartPtr<map<string,MyClass> > threadsafeCache(&cache);
doSomethingInAnotherThread1 (threadsafeCache);
doSomethingInAnotherThread2 (threadsafeCache);


Here follow some more details about the implementation...

  • To just perform some action (e.g. lock_.lock() ) BEFORE the "intercepted" call, you don't need an extra auxiliary object (e.g. LockSmartPtrAux), but only to invoke it before returning the pointee.

  • However, to perform some action (e.g. lock_.unlock() ) just AFTER the intercepted call and before the return, you have to place that action in the destructor of an auxiliary object and use operator-> trick.



This technique can be used to implement a rudimentary form of Aspect Oriented Programming in C++ by expressing cross-cutting concerns in these "wrapper" classes.

That's all the C++ trickery for today, hope you liked it.

NOTE: my implementation of LockSmartPtr assumes your compiler implements the name return value optimization (NRVO); if this does not hold true and you use non recursive locks, you will probably get a nasty deadlock due to temporary copies of LockSmartPtrAux locking an already locked mutex from the same thread. Check out Stroutstrup's paper to see how (managing lock ownership, forbidding assignment) he tackles this and other issues.

No comments:

Post a Comment