views:

95

answers:

4

Suppose I have an autolocker class which looks something like this:

template <T>
class autolocker {
public:
    autolocker(T *l) : lock(l) {
        lock->lock();
    }

    ~autolocker() {
        lock->unlock();
    }
private:
    autolocker(const autolocker&);
    autolocker& operator=(const autolocker&);
private:
    T *lock;
};

Obviously the goal is to be able to use this autolocker with anything that has a lock/unlock method without resorting to virtual functions.

Currently, it's simple enough to use like this:

autolocker<some_lock_t> lock(&my_lock); // my_lock is of type "some_lock_t"

but it is illegal to do:

autolocker lock(&my_lock); // this would be ideal

Is there anyway to get template type deduction to play nice with this (keep in my autolocker is non-copyable). Or is it just easiest to just specify the type?

+1  A: 

autolocker is a class template, not a class. Your "this would be ideal" is showing something that doesn't make sense in C++.

jwismar
__If there's one thing I've learned in almost twenty years of programming in C++, it's that "there's no way to do this" usually is a temporary state of affairs.__ Of course, `autolocker` as presented was a template and so it wouldn't work, but three people came up with three different techniques to circumvent that obstruction. That's a typical situation and that's why people like to run such problems by peers: Someone might twist their head just far enough to see the problem from an angle nobody had looked at it from and comes up with a way to do the undoable. `:)`
sbi
+1  A: 

I think jwismar is correct and what you want is not possible with C++. However, a similar (not direct analogue) construct is possible with C++0x, using several new features (rvalues/moving and auto variable type):

#include <iostream>

template <typename T>
class autolocker_impl
{
public:
  autolocker_impl(T *l) : lock(l) {
    lock->lock();
  }

  autolocker_impl (autolocker_impl&& that)
    : lock (that.lock)
  {
    that.lock = 0;
  }

  ~autolocker_impl() {
    if (lock)
      lock->unlock();
  }
private:
  autolocker_impl(const autolocker_impl&);
  autolocker_impl& operator=(const autolocker_impl&);
private:
  T *lock;
};

template <typename T>
autolocker_impl <T>
autolocker (T* lock)
{
  return autolocker_impl <T> (lock);
}

struct lock_type
{
  void lock ()
  { std::cout << "locked\n"; }
  void unlock ()
  { std::cout << "unlocked\n"; }
};

int
main ()
{
  lock_type l;
  auto x = autolocker (&l);
}
doublep
Oh i see now my boolean flag wasn't necessary. I could just use the pointer as the "don't call unlock" flag, like you do in your code. +1 for the C++0x way.
Johannes Schaub - litb
+5  A: 

Yes you can use the scope-guard technique

struct autolocker_base {
    autolocker_base() { } 
protected:
    // ensure users can't copy-as it
    autolocker_base(autolocker_base const&) 
    { }

    autolocker_base &operator=(autolocker_base const&)
    { return *this; }
};

template <T>
class autolocker : public autolocker_base {
public:
    autolocker(T *l) : lock(l) {
        lock->lock();
    }

    autolocker(const autolocker& o)
      :autolocker_base(o), lock(o.lock)
    { o.lock = 0; }

    ~autolocker() {
        if(lock)
          lock->unlock();
    }

private:
    autolocker& operator=(const autolocker&);

private:
    mutable T *lock;
};

Then write a function creating the autolocker

template<typename T>
autolocker<T> makelocker(T *l) {
  return autolocker<T>(l);
}

typedef autolocker_base const& autolocker_t;

You can then write it like this:

autolocker_t lock = makelocker(&my_lock);

Once the const reference goes out of scope, the destructor is called. It doesn't need to be virtual. At least GCC optimizes this quite well.

Sadly, this means you have to make your locker-object copyable since you need to return it from the maker function. But the old object won't try to unlock twice, because its pointer is set to 0 when it's copied, so it's safe.

Johannes Schaub - litb
+1. A similar pattern is used by `boost::weak_ptr<>` and is really effective.
ereOn
Now you have to write `makelocker` instead of repeating the type. Sure, you don't have to name the type twice this way, but it's still cumbersome. I think this should be doable storing a pointer to an unlocking static member function instance. That would let you write `autolocker lock(` and be done. See my answer for a sketch of that.
sbi
Hm, having rethought it, i think that "autolocker_base" doesn't need to be publicly copyable. Only the "autolocker<T>" needs to be. Adapted accordingly.
Johannes Schaub - litb
+2  A: 

Obviously you can't get away with autolocker being a template, because you want to use it as a type, and templates must be instantiated in order to obtain types.

But type-erasure might be used to do what you want. You turn the class template into a class and its constructor into a member template. But then you'd have to dynamically allocate an inner implementation object.
Better, store a pointer to a function that performs the unlock and let that function be an instance of a template chosen by the templatized constructor. Something along these lines:

// Comeau compiles this, but I haven't tested it. 
class autolocker {
public:
    template< typename T >
    autolocker(T *l) : lock_(l), unlock_(&unlock<T>) { l->lock(); }

    ~autolocker()                                    { unlock_(lock_); }
private:
    autolocker(const autolocker&);
    autolocker& operator=(const autolocker&);
private:
    typedef void (*unlocker_func_)(void*);

    void *lock_;
    unlocker_func_ unlock_;

    template <typename T>
    static void unlock(void* lock)                   { ((T*)lock)->unlock(); }
};

I haven't actually tried this and the syntax might be wrong (I'm not sure how to take the address of a specific function template instance), but I think this should be doable in principle. Maybe someone comes along and fixes what I got wrong.

I like this a lot more than the scope guard, which, for some reason, I never really liked at all.

sbi
Wow, this is a sweet way to do it :) I like it better than my scope-guard thing too, since it avoids the copy-constructor/mutable madness :) I wonder how compilers' optimizers handle it? Would be interesting to see :)
Johannes Schaub - litb
@Johannes: Bear in mind that I haven't even compiled this. Do you think it would work? Anyway, I think compilers are often not very good at inlining functions called through pointers. But I guess this wouldn't matter all that much since calling `unlock()` on the lock will most likely dominate the run-time. Or what optimization were you thinking about?
sbi
Dang, Comeau's online compiler doesn't like it. Anyway, if taking the address of a static member function template doesn't work this way (it's Saturday night close to 2am here and I'm not going to work this out), putting it into a static member function of a local class template might do the trick.
sbi
@sbi yes i think this technique will work. I remember GCC had problems in the past of taking the address of a specific function template specialization, but to be fair the Standard in C++03 times didn't state that it would work. C++0x added rules that make this work. IIRC, these rules are implemented retroactively by current C++03 compilers i'm aware of, though (GCC, clang and comeau). See http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#115 . Without that rule, even if you were to cast to `unlocker_func_`, you would never yield a pointer since `T` couldn't be deduced.
Johannes Schaub - litb
@sbi, well after changing the typos it compiles fine for me on comeau :)
Johannes Schaub - litb
@Johannes: Yeah, eventually it bothered me too much and I went and massaged it until Comeau agreed with it. _But I'm not going to test this now._ I'm going to go to bed instead. `:)`
sbi