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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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.
1 2 3 4 5 6 7 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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:
1 2 3 4 5 6 7 | 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!