总是在C ++ 11中声明std :: mutex是可变的?
在看过Herb Sutter的谈话之后, 你不知道const和mutable ,我想知道我是否应该总是把一个互斥量定义为可变的? 如果是的话,我猜同样适用于任何同步的容器(例如, tbb::concurrent_queue
)?
一些背景:在他的演讲中,他指出const == mutable ==线程安全,而std::mutex
是每个定义线程安全的。
还有关于这个谈话的相关问题, const在C ++ 11中是不是线程安全的 。
编辑:
在这里 ,我find了一个相关的问题(可能是重复的)。 不过,它在C ++ 11之前被问到了。 也许这是有所作为的。
不,但是,大部分时间他们会。
尽pipe将const
看作“线程安全”并且mutable
变为“(已经)线程安全”是有帮助的,但const
仍然与承诺“我不会改变这个值”的概念有着根本的联系。 它总是会的。
我有一个很长的思路,所以对我很感兴趣。
在我自己的编程中,我把const
放在了任何地方。 如果我有价值,除非我说我想要改变它,否则是件坏事。 如果你试图有目的地修改一个const对象,你会得到一个编译时错误(容易修复,无法传送结果!)。 如果你不小心修改了一个非const对象,你会得到一个运行时编程错误,一个已编译应用程序中的错误,以及一个头痛的问题。 所以最好在前面犯错,并保持const
。
例如:
bool is_even(const unsigned x) { return (x % 2) == 0; } bool is_prime(const unsigned x) { return /* left as an exercise for the reader */; } template <typename Iterator> void print_special_numbers(const Iterator first, const Iterator last) { for (auto iter = first; iter != last; ++iter) { const auto& x = *iter; const bool isEven = is_even(x); const bool isPrime = is_prime(x); if (isEven && isPrime) std::cout << "Special number! " << x << std::endl; } }
为什么is_even
和is_prime
的参数types标记为const
? 因为从实施的angular度来看,改变我testing的数字将是一个错误! 为什么const auto& x
? 因为我不打算改变这个价值,所以我希望编译器在我的时候大吼大叫。 isEven
和isEven
一样:这个testing的结果不应该改变,所以执行它。
当然, const
成员函数只是一种赋予const T*
forms的方法。 它说“如果我要改变我的一些成员,这将是一个错误”。
mutable
说“除了我”。 这是“逻辑常量”的“旧”概念的来源。 考虑一下他给出的常见用例:一个互斥对象成员。 你需要locking这个互斥锁,以确保你的程序是正确的,所以你需要修改它。 但是,您不希望函数是非const的,因为修改其他成员将是错误的。 所以你把它设为const
并把互斥量标记为mutable
。
这与线程安全无关。
我认为用新的定义取代上面的旧观念是远远不够的; 他们只是从另一个angular度来赞美它,即线程安全。
现在Herb认为,如果你有const
函数,它们需要是线程安全的,以便标准库可以安全地使用。 作为一个必然结果,唯一应该标记为mutable
成员是那些已经是线程安全的成员,因为它们可以从const
函数中修改:
struct foo { void act() const { mNotThreadSafe = "oh crap! const meant I would be thread-safe!"; } mutable std::string mNotThreadSafe; };
好的,所以我们知道线程安全的东西可以被标记为mutable
,你问:他们应该是?
我认为我们必须同时考虑两种观点。 从Herb的新观点来看,是的。 它们是线程安全的,因此不需要被函数的常量所束缚。 但只是因为他们可以安全地从const
的限制中免除,并不意味着他们必须是。 我仍然需要考虑:如果我修改了这个成员,是否会出现执行错误? 如果是这样,它不需要变化!
这里有一个粒度问题:一些函数可能需要修改可能会被修改的成员,而另一些则不需要。 这就像只想要一些function有朋友般的访问,但是我们只能帮助整个class级。 (这是一个语言devise问题。)
在这种情况下,你应该在mutable
的方面犯错。
当Herb给一个const_cast
例子声明它是安全的时,Herb稍微const_cast
。 考虑:
struct foo { void act() const { const_cast<unsigned&>(counter)++; } unsigned counter; };
在大多数情况下这是安全的,除非foo
对象本身是const
:
foo x; x.act(); // okay const foo y; y.act(); // UB!
这在SO的其他地方已经介绍过了,但是const foo
暗示counter
成员也是const
,修改一个const
对象是未定义的行为。
这就是为什么你应该在mutable
的方面犯错: const_cast
不能给你相同的保证。 有counter
被标记为mutable
,它不会是一个const
对象。
好吧,如果我们需要它在一个地方mutable
,我们需要它在任何地方,我们只需要小心,在我们不这样做的情况下。 当然,这意味着所有线程安全的成员应该被标记为mutable
吗?
那么不,因为并不是所有线程安全的成员都有内部同步。 最微不足道的例子是某种包装类(并不总是最佳实践,但它们存在):
struct threadsafe_container_wrapper { void missing_function_I_really_want() { container.do_this(); container.do_that(); } const_container_view other_missing_function_I_really_want() const { return container.const_view(); } threadsafe_container container; };
在这里,我们正在包装threadsafe_container
并提供我们想要的另一个成员函数(在实践中将作为一个自由函数更好)。 在这里没有必要mutable
,从旧的angular度来看,正确性完全胜过:在一个函数中,我正在修改容器,这没关系,因为我没有说我不会 (省略const
),而在另一个const
,没有修改容器, 并确保我遵守承诺 (省略mutable
)。
我认为Herb认为大多数情况下,我们会使用mutable
我们也使用某种内部(线程安全)的同步对象,我同意。 他的观点在大多数情况下都适用。 但是,有些情况下,我只是碰巧有一个线程安全的对象,只是把它当作另一个成员; 在这种情况下,我们回到了const
的旧的和基本的使用。
我只是看了这个讲话,而且我不完全同意赫特·萨特所说的话。
如果我理解正确,他的论点如下:
-
[res.on.data.races]/3
强制要求与标准库一起使用的types – 非const成员函数必须是线程安全的。 -
所以
const
相当于线程安全的。 -
如果
const
等价于线程安全,mutable
必须相当于“相信我,即使这个variables的非const成员是线程安全的”。
在我看来,这个论点的三个部分都是有缺陷的(第二部分是严重缺陷的)。
1
的问题是[res.on.data.races]
提供了标准库中types的要求,而不是标准库使用的types。 也就是说,我认为将[res.on.data.races]
解释为对标准库使用的types也是合理的(但不是完全明确的),因为对于图书馆来说实际上是不可能的如果const
成员函数能够修改对象,则通过const
引用来保持不修改对象的要求。
2的关键问题是虽然它是真的(如果我们接受1
),那么const
必须暗含线程安全,但是线程安全并不意味着const
,因此二者不是等价的。 const
仍然意味着“逻辑不可变”,这就是“逻辑不变性”的范围已经扩大到要求线程安全。
如果我们把const
和线程安全等同起来的话,我们就失去了const
一个很好的特性,那就是它允许我们通过查看哪些值可以被修改,
//`a` is `const` because `const` and thread-safe are equivalent. //Does this function modify a? void foo(std::atomic<int> const& a);
此外, [res.on.data.races]
的相关部分谈到了“修改”,可以从更一般的意义上来理解“外部可观察的方式的变化”,而不仅仅是“不安全的方式“。
3
的问题只不过是只有2
才是真的, 2
是有严重缺陷的。
所以把这个应用到你的问题 – 不,你不应该让每个内部同步的对象mutable
。
在C ++ 11中,和C ++ 03一样,`const`意思是“逻辑上不可变的”,“可变的”意思是“可以改变,但是这种改变不会在外部观察到”。 唯一的区别是在C ++ 11中,“逻辑不可变”已经被扩展为包含“线程安全”。
对于不影响对象的外部可见状态的成员variables,应该保留mutable
。 另一方面(这是Herb Sutter在他的演讲中的关键点),如果你有一个成员由于某种原因是可变的,那么这个成员必须在内部同步,否则你冒着使const
不暗含线程安全,这会导致标准库的未定义的行为。
让我们来谈谈const
的变化。
void somefunc(Foo&); void somefunc(const Foo&);
在C ++ 03和之前版本中,与非const
版本相比, const
版本为调用者提供了额外的保证。 它承诺不会修改它的参数,通过修改我们的意思是调用Foo
的非const成员函数(包括赋值等),或者将它传递给期望非常量参数的函数,或者对其暴露的非可变参数数据成员。 somefunc
限制自己在Foo
上进行const
操作。 而额外的保证是完全片面的。 调用者和Foo
提供者都不必为了调用const
版本而做任何特殊的事情。 任何能够调用非const
版本的人都可以调用const
版本。
在C ++ 11中,这个变化。 const
版本仍然为调用者提供相同的保证,但现在它带有一个价格。 Foo
的提供者必须确保所有的const
操作都是线程安全的 。 或者至less它必须这样做,当somefunc
是一个标准的库函数。 为什么? 因为标准库可能会并行操作,并且会在任何事情上调用const
操作,而不需要任何额外的同步。 因此,用户必须确保不需要额外的同步。 当然,在大多数情况下这不是问题,因为大多数类没有可变成员,大多数const
操作不会触及全局数据。
那么现在有什么变化呢? 和以前一样! 也就是说,这个数据是非常量的,但它是一个实现细节,我保证它不会影响可观察行为。 这意味着不,你不需要把所有的东西都标记为mutable
,就像你没有在C ++ 98中做的一样。 那么当你应该标记一个数据成员是mutable
? 就像在C ++ 98中一样,当你需要从const
方法中调用它的非const
操作时,你可以保证它不会破坏任何东西。 重申:
- 如果您的数据成员的物理状态不影响对象的可观察状态
- 它是线程安全的(内部同步)
- 那么你可以(如果你需要!)继续前进,宣布它是
mutable
。
第一个条件是强加的,就像在C ++ 98中一样,因为包括标准库在内的其他代码可能会调用你的const
方法,并且没有人应该观察到这样的调用导致的任何变化。 第二个条件是在那里,这是C ++ 11中的新特性,因为这样的调用可以asynchronous进行。
被接受的答案涵盖了这个问题,但值得一提的是,Sutter已经改变了错误地提示const == mutable ==线程安全的幻灯片。 导致幻灯片更改的博客文章可以在这里find:
Sutter在C ++ 11中遇到了什么问题
TL:DR Const和Mutable都意味着线程安全,但是在程序中可以或不可以改变的含义是不同的。