Whose Lock Is It, Anyway?
There's a weasel word in thereI mentioned that ATMWithdrawal is reasonably safe. It's not really safe because there's no enforcement that the StrictLock<BankAccount> object locks the appropriate BankAccount object. The type system only ensures that some BankAccount object is locked. For example, consider the following phony implementation of ATMWithdrawal:
void ATMWithdrawal(BankAccount& acct, int sum) { BankAccount fakeAcct("John Doe", "123-45-6789"); StrictLock<BankAccount> guard(fakeAcct); acct.Withdraw(sum, guard); acct.Withdraw(2, guard); }
This code compiles warning-free but obviously doesn't do the right thingit locks one account and uses another.
It's important to understand what can be enforced within the realm of the C++ type system and what needs to be enforced at runtime. The mechanism we've put in place so far ensures that some BankAccount object is locked during the call to BankAccount::Withdraw(int, StrictLock<BankAccount>&). We must enforce at runtime exactly what object is locked.
If our scheme still needs runtime checks, how is it useful? An unwary or malicious programmer can easily lock the wrong object and manipulate any BankAccount without actually locking it.
First, let's get the malice issue out of the way. C is a language that requires a lot of attention and discipline from the programmer. C++ made some progress by asking a little less of those, while still fundamentally trusting the programmer. These languages are not concerned with malice (as Java is, for example). After all, you can break any C/C++ design simply by using casts "appropriately" (if appropriately is an, er, appropriate word in this context).
The scheme is useful because the likelihood of a programmer forgetting about any locking whatsoever is much greater than the likelihood of a programmer who does remember about locking, but locks the wrong object.
Using StrictLocks permits compile-time checking of the most common source of errors, and runtime checking of the less frequent problem.
Let's see how to enforce that the appropriate BankAccount object is locked. First, we need to add a member function to the StrictLock class template. The StrictLock<T>::GetObject function returns a reference to the locked object.
template <class T> class StrictLock { ... as before ... public: T* GetObject() const { return &obj_; } };
Second, BankAccount needs to compare the locked object against this:
class BankAccount { int balance_; public: void Deposit(int amount, StrictLock<BankAccount>& guard) { // Externally locked if (guard.GetObject() != this) throw "Locking Error: Wrong Object Locked"; balance_ += amount; } ... };
The overhead incurred by the test above is much lower than locking a recursive mutex for the second time.