Tuesday, April 13, 2010

Backdoor C++ Idiom - part III

I will end this suite around the backdoor C++ idiom with implementation variants.

Implementation

Analyze applicability
First of all, the designer must be sure the pattern is applicable to the class by checking the applicability clauses.

Identify the part of the Component that must be used with caution and its protocol
This is a class specific task that must be done with caution. Only the actual safe operations must be public. Operations and data which are needed to open the interface and which must be used with caution by a safe adapter class should be private and usually annotated as such.
class  mutex {
public:
  // ... 
  typedef mutex_backdoor backdoor;
  friend class backdoor ;
  // list of predefined safe adapters
  typedef mutex_scoped_lock scoped_lock;
private: // backdoor 
  void  lock();// backdoor 
  void  unlock();// backdoor 
  // ... 
};
If the Component is a model of a Concept, which has already a backdoor through a template class and safe component adapters, use them.
class  mutex {
public:
  // ... 
  typedef backdoor<mutex> backdoor;
  friend class backdoor ;
  typedef scoped_lock<mutex> scoped_lock;
  // ... 
};
Safe guaranties: unlock must be called if lock is called, i.e. lock/unlock are well balanced.

Define a backdoor interface and safe guaranties for Component

This class gives you a back-door access to your class. There are at least two possibilities: by class instance or static
  • instance interface: an instance of a backdoor is initialized with an instance of the class, giving back-door access


  • static interface: this could be more efficient with compilers that haven't a good optimizer.
Instance interface
class  mutex_backdoor {
public:
  mutex_backdoor ( mutex & mtx) : mtx_(mtx) {}
  void lock() {
    mtx_.lock();
  }
  void unlock() {
    mtx_.unlock();
  }
private:
  mutex & mtx_;
};
The SafeAdapter client could write
mutex::backdoor(mtx).lock();

Static interface
struct mutex_backdoor {
  static void lock(mutex& mtx) {
    mtx.lock();
  }
  static void unlock(mutex& mtx) {
    mtx.unlock();
  }
};
The SafeAdapter client could write
mutex_backdoor::lock(mtx); 
 
When applying the idiom to generic programming, the backdoor should be applied to a concept. So, the backdoor for a mutex class can be a template class, which is the backdoor for models of the Lockable concept, and then the backdoor is written only once. For brevity purposes only the by instance class interface is shown:
template <typename Lockable>
class  backdoor {
  backdoor( Lockable & mtx) : mtx_(mtx) {}
  void lock() {
    mtx_.lock();
  }
  void unlock() {
    mtx_.unlock();
  }
private:
  Lockable& mtx_;
};
And the SafeAdapter client could write
backdoor<mutex>(mtx).lock();

The use of the backdoor template class can be simplified by using a backdoor generator function:
template <typename Lockable>
typename Lockable::backdoor backdoor_access( Lockable& mtx)
{
  return Lockable:: backdoor(mtx);
}
And the SafeAdapter client could write
backdoor_access(mtx).lock();

Define a set of Component Safe Adapters that provides a safe interface to your Component

This set of ComponentSafeAdapters must at least provide the same functionalities as the Component would provide if the interface of the backdoor were public in a safe way. For brevity purposes, only the generic safe component adapter accessing a per instance backdoor is shown:
 
// ensures that unlock is called on destruction when owned 
// note that this is different to ensure that the mutex is 
// locked during the lifetime of the scoped variable. 
template <typename Lockable>
class  scoped_lock {
public :
  scoped_lock( Lockable & mtx, bool owns=true) // [1] 
    : mtx_(mtx),
    , owned(false) {
    if (owns) lock();// [2] 
  }
  ~scoped_lock(mutex&) {
    if (owned) { // [5] 
      backdoor_access(mtx_).unlock();// [4] 
    }
  }
  void  lock() {
    if (!owned) { // [3] 
      backdoor_access(mtx_).lock();// [4] 
      owned = true; // [3] 
    } else {
      // ERROR // [6] 
    }
  }
  void  unlock() {
    if (owned) { // [3] 
      backdoor_access(mtx_).unlock();// [4] 
      owned = false; // [3] 
    } else {
      // ERROR // [6] 
    } 
  } 
 bool  owned() {
    return owned;
  }
private :
  bool owned;
  mutex& mtx_;
};
  1. The interface allows to lock the mutex on construction
  2. Use of an internal function
  3. The use of the lock/unlock is controlled by the owned variable
  4. Use of backdoor interface using the backdoor_access function.
  5. On destruction the mutex is unlocked if owned.
  6. Error has not been detailed.
Use the component and the Component Safe Adapters
The use of the ComponentSafeAdapter is obvious.
 
mutex guard;
{
  // guard.lock() compilation ERROR
  mutex::scoped_lock scoped( guard );
  // enter the critical section
  // ...
  // guard.unlock() compilation ERROR
}

Identify other Component Safe Adapters

Different Component Safe Adapters can be identified. Associated to the Lockable concept there are:
  • scoped_strict_lock: ensures that the mutex is owned during the lifetime of the scoped variable.
  • locking_ptr: this smart pointer locks the mutex when de-referencing the pointer, call the function, and then unlocks the mutex.
  • scoped_locking_ptr: this smart pointer locks the mutex on construction, defines the operator->, and unlocks the mutex on destruction.
  • scoped_reverse_lock: unlocks the mutex on construction, and locks the mutex on destruction. For recursive mutex, this do not implies that on the scoped block the mutex is unlocked.
The designer of such ComponentSafeAdapter must be aware of the possible interactions between the set of adapters, and must specify which are the guaranties. Next follows some interactions between ComponentSafeAdapters.
mutex guard;
void f()
{
  scoped_strict_lock strict( guard );
  // the mutex is locked 
  {
    // call to a self locked function 
    // this will work if the mutex is recursive 
    g();
    // pass a strict lock to a function. The “other” object has been 
    // constructed with an external locking mechanism and its correct 
    // behavior depends on the caller, which owns the mutex. 
    // Having a strict lock ensures this. 
    other.h(strict);
  }
  {
    // the user is aware that on this block the mutex must be unlocked but a strict guards is not a scoped_lock 
    scoped_reverse_lock reverse( strict ); // ERROR do not compile 
    // the mutex is unlocked 
    // ... 
    // the mutex is locked  
  }
  // the mutex is still locked 
  h( strict );
}
void g() {
scoped_lock scoped( guard );
  // mutex is locked 
  // ... 
  {
    scoped_reverse_lock reverse( scoped );
    // the mutex is unlocked 
    // ... 
    // the mutex is locked 
  }
  // mutex is locked 
  // ... 
} 
 
Know Uses
  • Boost::Threads library (v1. 34.1): The synchronization part of this library used the backdoor for the model of the Lockable concept with a static interface (lock_ops) defined on a nested namespace named ‘detail'. It is admitted that the ‘detail' namespace is private namespace and do not make part of the interface. The backdoor class specified more explicitly which is the intent of this kind of classes.
Related Patterns
  • RAII (Resource Initialization Is Initialization): The component safe adapters usually use the RAII idiom in order to reach on destruction a stable state for the Component.
  • Extension object: The ComponentSafeAdapter can be seen as an Extension objects of the Component.

No comments:

Post a Comment