The containers' interfaces have simply not been designed with this objective. For the interfaces they use, a lock visible to the client is really the only way you could accomplish this while guaranteeing correctness and predictable behaviour. It would also be terribly inefficient because the number of acquisitions would be very high (relative to a good implementation).
Solution 1
Pass by value (where applicable).
Solution 2
Create a collection of simple bolt-on implementations that you can use to pass containers while holding a scope lock (consider it pseudo c++):
template <typename TCollection>
class t_locked_collection {
public:
t_locked_collection(TCollection& inCollection, t_lock& lock) : collection(inCollection), d_lock(lock), d_nocopy() {
}
TCollection& collection;
// your convenience stuff
private:
t_scope_lock d_lock;
t_nocopy d_nocopy;
};
then the caller pairs the lock with the collection, and then you update your interfaces over to use (pass by) the container type where appropriate. It's just a poor man's class extension.
This locked container is one simple example, and there are a few other variants. This is the route I chose because it really allows you to use the granularity level which is ideal for your program, even though it not as transparent (syntactically) as locked methods. It's also relatively easy to adapt existing programs. At least it behaves in a predictable manner, unlike collections with internal locks.
Another variant would be:
template <typename TCollection>
class t_lockable_collection {
public:
// ...
private:
TCollection d_collection;
t_mutex d_mutex;
};
// example:
typedef t_lockable_collection<std::vector<int> > t_lockable_int_vector;
...where a type similar to t_locked_collection
could be used to expose the underlying collection. Not to imply that approach is foolproof, just fool resistant.