不,但是,大多数时候他们会。
虽然将其const
视为“线程安全”和mutable
“(已经)线程安全”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
标记const
?因为从实现的角度来看,更改我正在测试的数字将是一个错误!为什么const auto& x
?因为我不打算改变那个值,如果我这样做了,我希望编译器对我大喊大叫。与isEven
and相同isPrime
:此测试的结果不应更改,因此请强制执行。
当然,const
成员函数只是一种给出this
形式类型的方法const T*
。它说“如果我要改变我的一些成员,那将是一个实施错误”。
mutable
说“除了我”。这就是“逻辑上的 const”的“旧”概念的来源。考虑他给出的常见用例:互斥体成员。您需要锁定此互斥锁以确保您的程序正确,因此您需要对其进行修改。但是,您不希望该函数是非常量的,因为修改任何其他成员都是错误的。所以你做了它const
并将互斥锁标记为mutable
.
这些都与线程安全无关。
我认为说新定义取代了上面给出的旧想法有点过分了。它们只是从另一个角度补充它,即线程安全。
现在 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 约束。但仅仅因为他们可以安全地摆脱限制const
并不意味着他们必须这样做。我仍然需要考虑:如果我修改了那个成员,会不会是一个错误的实现?如果是这样,它不需要mutable
!
这里有一个粒度问题:一些函数可能需要修改可能的mutable
成员,而另一些则不需要。这就像只希望某些功能具有类似朋友的访问权限,但我们只能为整个班级加好友。(这是一个语言设计问题。)
在这种情况下,您应该在mutable
.
const_cast
Herb 举了一个例子并宣布它是安全的,他说得有点太松散了。考虑:
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
,从旧的角度来看,正确性完全胜过:在一个函数中,我正在修改容器,这没关系,因为我没有说我不会(省略const
),而在另一个函数中我不是修改容器并确保我信守承诺(省略mutable
)。
我认为 Herb 在争论我们使用的大多数情况下,我们mutable
也在使用某种内部(线程安全)同步对象,我同意。因此,他的观点大部分时间都有效。但是在某些情况下,我只是碰巧有一个线程安全的对象,只是把它当作另一个成员;在这种情况下,我们依靠const
.