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! 🙂