Herb Sutter gave a great talk at the C++ and Beyond 2012 conference on concurrency in C++11.
The talk had an interesting interlude on how to wrap all functions performed on an existing type T in order to inject any desired behavior, and that's what this post is all about.
The pattern basically looks like this:
template <typename T> class wrap { public: wrap(T wrapped = T{}) : m_wrapped{wrapped} {} // operator() takes any code that accepts a wrapped T // will usually be a lambda template <typename F> auto operator()(F func) -> decltype(func(m_wrapped)) { // do any pre-function call wrapper work auto result = func(m_wrapped); // do any post-function call wrapper work return result; } private: T m_wrapped; // ... other state required by wrapper };
This wrap class template can now be used by instantiating it and calling operator() on it with an anonymous lambda function that accepts the wrapped type as single argument.
wrap<X> wrapper; wrapper([](X& x) { x.call_something(foo, bar); x.do_something_else(); std::cout << x.print() << std::endl; });
A concrete example of using this pattern is a monitor wrapper class, which is similar to the synchronized keyword in Java.
A monitor wraps every function call (or a group of function calls) on an existing class in a mutex lock to make the class thread-safe.
template <typename T> class monitor { public: monitor(T wrapped = T{}) : m_wrapped{wrapped} {} template <typename F> auto operator()(F func) -> decltype(func(m_wrapped)) { std::lock_guard<mutex> lock{m_mutex}; return func(m_wrapped); } private: mutable T m_wrapped; mutable std::mutex m_mutex; };
Note that m_wrapped is mutable, which is required so that func(m_wrapped) works if func takes the T as a non-const reference T&.
Mutable in C++11 means thread-safe, which means either bitwise const or internally synchronized. Our mutex lock guard guarantees that by the time the m_wrapped is used in func(m_wrapped) it will be internally sychronized, so making m_wrapped mutable is perfectly acceptable.
monitor<string> s = "start\n"; std::vector<std::future<void>> tasks; // start a bunch of tasks for (int i = 0; i < 5; ++i) { tasks.push_back(async([&,i] { // single transaction, synchronized modification of s s([=](string& s) { s += "transaction " + to_string(i) + " of 5"; s += '\n'; }); // do some more work // single transaction, synchronized read of s s([](string& s) { std::cout << s; }); }); } // join all tasks for (auto& task : tasks) task.wait();
Note that T used in wrap
So it is perfectly valid to do:
monitor<ostream&> sync_cout{std::cout}; sync_cout([=](ostream& cout) { cout << "Writing stuff to cout..." << std::endl; cout << "in a synchronized way" << std::endl; });
That's a wrap, folks! 🙂