隐藏的问题是:你将保护你的类的互斥锁放在哪里?
总而言之,假设您要读取受互斥体保护的对象的内容。
“read”方法在语义上应该是“const”,因为它不会改变对象本身。但是要读取值,需要先锁定一个互斥体,提取值,然后解锁互斥体,这意味着互斥体本身必须被修改,也就是说互斥体本身不能是“const”。
如果互斥锁是外部的
然后一切正常。对象可以是“const”,互斥体不必是:
Mutex mutex ;
int foo(const Object & object)
{
Lock<Mutex> lock(mutex) ;
return object.read() ;
}
恕我直言,这是一个糟糕的解决方案,因为任何人都可以重用互斥锁来保护其他东西。包括你。事实上,您会背叛自己,因为如果您的代码足够复杂,您只会对这个或那个互斥体究竟在保护什么感到困惑。
我知道:我是那个问题的受害者。
如果互斥锁是内部的
出于封装目的,您应该将互斥锁尽可能靠近它所保护的对象。
通常,您会编写一个内部带有互斥锁的类。但迟早,您需要保护一些复杂的 STL 结构,或者其他人编写的没有内部互斥锁的任何东西(这是一件好事)。
这样做的一个好方法是使用添加互斥功能的继承模板派生原始对象:
template <typename T>
class Mutexed : public T
{
public :
Mutexed() : T() {}
// etc.
void lock() { this->m_mutex.lock() ; }
void unlock() { this->m_mutex.unlock() ; } ;
private :
Mutex m_mutex ;
}
这样,你可以写:
int foo(const Mutexed<Object> & object)
{
Lock<Mutexed<Object> > lock(object) ;
return object.read() ;
}
问题在于它不起作用,因为object 是 const,而锁对象正在调用非 const lock 和 unlock 方法。
困境
如果您认为const 仅限于按位 const 对象,那么您就完蛋了,必须回到“外部互斥解决方案”。
解决方案是承认const 更像是一个语义限定符(就像volatile 用作类的方法限定符时一样)。您隐藏了该类不完全为 const 的事实,但仍确保提供一个实现,以保证在调用 const 方法时不会更改该类的有意义部分。
然后你必须声明你的互斥锁是可变的,锁定/解锁方法const:
template <typename T>
class Mutexed : public T
{
public :
Mutexed() : T() {}
// etc.
void lock() const { this->m_mutex.lock() ; }
void unlock() const { this->m_mutex.unlock() ; } ;
private :
mutable Mutex m_mutex ;
}
内部互斥体解决方案是一个很好的解决方案,恕我直言:一方面必须将对象声明为彼此靠近,另一方面将它们都聚合在包装器中,这最终是一回事。
但是聚合有以下优点:
- 更自然(在访问之前锁定对象)
- 一个对象,一个互斥体。由于代码风格迫使您遵循此模式,因此它降低了死锁风险,因为一个互斥锁将只保护一个对象(而不是您不会真正记住的多个对象),一个对象将仅受一个互斥锁保护(而不是由需要按正确顺序锁定的多个互斥体)
- 上面的互斥类可以用于任何类
因此,让您的互斥锁尽可能靠近互斥锁对象(例如,使用上面的 Mutexed 构造),并为互斥锁使用 mutable 限定符。
编辑 2013-01-04
显然,Herb Sutter 也有同样的观点:他对 C++11 中const 和mutable 的“新”含义的介绍非常有启发性:
http://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/