总是在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_evenis_prime的参数types标记为const ? 因为从实施的angular度来看,改变我testing的数字将是一个错误! 为什么const auto& x ? 因为我不打算改变这个价值,所以我希望编译器在我的时候大吼大叫。 isEvenisEven一样:这个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的旧的和基本的使用。

我只是看了这个讲话,而且我不完全同意赫特·萨特所说的话。

如果我理解正确,他的论点如下:

  1. [res.on.data.races]/3强制要求与标准库一起使用的types – 非const成员函数必须是线程安全的。

  2. 所以const相当于线程安全的。

  3. 如果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都意味着线程安全,但是在程序中可以或不可以改变的含义是不同的。