
On Jul 23, 2004, at 8:10 AM, Peter Dimov wrote:
The main reason I'm unconvinced about these "useful helper" constructors is that all of the use cases I see on the list are totally bogus. The only real use case for a try_lock (lock_both) does not need helper constructors. The only other real use case templated on a lock (cv::wait) accepts a lock variable and does not need helper constructors.
I may be missing something, of course, but the conservative approach is to resist the temptation to provide the "but someone might find them useful!!~1" parts until we see how well they compare to the "no frills" approach in real code.
I sat down to write a realistic example of when a "try_lock" constructor might be sufficiently desirable to warrant its existence: Consider a program with a list a jobs that needs doing, in no particular order (maybe updating various parts of a display - clock, temperature, internet status, whatever). A reasonable strategy would be to have a vector<Job> where each Job has a functor, a mutex, and a bool indicating whether or not it needed running. You could throw several threads at the function which executes each job in the vector<Job>, and each thread could try to get the mutex, and if it failed, just move on to the next job. Such a strategy might make good use of multiple processors. Here's what I came up with: template <class F, class Mutex> struct job { typedef F Func; typedef Mutex Mutex; F f_; Mutex* mut_; bool needs_executing_; }; template <class Job> void do_jobs(std::vector<Job>& v) { typedef typename Job::Mutex Mutex; typedef typename Mutex::scoped_lock Lock; for (typename std::vector<Job>::iterator i = v.begin(), e = v.end(); i != e; ++i) { Lock lock(*i->mut_, try_lock); if (lock && i->needs_executing_) { i->needs_executing_ = false; i->f_(); } } } Then I rewrote do_jobs for the case where I have no try-lock ctor: template <class Job> void do_jobs2(std::vector<Job>& v) { using namespace Metrowerks; typedef typename Job::Mutex Mutex; typedef typename Mutex::scoped_lock Lock; for (typename std::vector<Job>::iterator i = v.begin(), e = v.end(); i != e; ++i) { Lock lock(*i->mut_, defer_lock); if (lock.try_lock() && i->needs_executing_) { i->needs_executing_ = false; i->f_(); } } } You really have to squint to see the difference. ... And I just wrote AND erased several paragraphs proposing that we eliminate the try and timed constructors, but also how I did not feel strongly either way! :-) But I just noticed a difference (apparently I didn't squint enough the first time ;-) ). The loop in do_jobs is no-throw if i->f_() is no-throw. The same loop in do_jobs2 should be no-throw as well, and it is, but the compiler doesn't know it. That is, the statement: lock.try_lock() hides the test: if (locked_) throw lock_error(); This is an unnecessary run time inefficiency. And for a compiler scanning the code to determine whether or not it might throw (a reasonable optimization step), do_jobs may come up no-throw whereas do_jobs2 probably won't (there's no way one can tag try_lock() as a function that won't throw). The code size hit may be the more important consideration (over the run time hit). -Howard Flipping like a fish on the dock