问题
After watching Herb Sutter's talk You Don't Know const and mutable, I wonder whether I should always define a mutex as mutable? If yes, I guess the same holds for any synchronized container (e.g., tbb::concurrent_queue
)?
Some background: In his talk, he stated that const == mutable == thread-safe, and std::mutex
is per definition thread-safe.
There is also related question about the talk, Does const mean thread-safe in C++11.
Edit:
Here, I found a related question (possibly a duplicate). It was asked before C++11, though. Maybe that makes a difference.
回答1:
No. However, most of the time they will be.
While it's helpful to think of const
as "thread-safe" and mutable
as "(already) thread-safe", const
is still fundamentally tied to the notion of promising "I won't change this value". It always will be.
I have a long-ish train of thought so bear with me.
In my own programming, I put const
everywhere. If I have a value, it's a bad thing to change it unless I say I want to. If you try to purposefully modify a const-object, you get a compile-time error (easy to fix and no shippable result!). If you accidentally modify a non-const object, you get a runtime programming error, a bug in a compiled application, and a headache. So it's better to err on the former side and keep things const
.
For example:
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;
}
}
Why are the parameter types for is_even
and is_prime
marked const
? Because from an implementation point of view, changing the number I'm testing would be an error! Why const auto& x
? Because I don't intend on changing that value, and I want the compiler to yell at me if I do. Same with isEven
and isPrime
: the result of this test should not change, so enforce it.
Of course const
member functions are merely a way to give this
a type of the form const T*
. It says "it would be an error in implementation if I were to change some of my members".
mutable
says "except me". This is where the "old" notion of "logically const" comes from. Consider the common use-case he gave: a mutex member. You need to lock this mutex to ensure your program is correct, so you need to modify it. You don't want the function to be non-const, though, because it would be an error to modify any other member. So you make it const
and mark the mutex as mutable
.
None of this has to do with thread-safety.
I think it's one step too far to say the new definitions replace the old ideas given above; they merely compliment it from another view, that of thread-safety.
Now the point of view Herb gives that if you have const
functions, they need to be thread-safe to be safely usable by the standard library. As a corollary of this, the only members you should really mark as mutable
are those that are already thread-safe, because they are modifiable from a const
function:
struct foo
{
void act() const
{
mNotThreadSafe = "oh crap! const meant I would be thread-safe!";
}
mutable std::string mNotThreadSafe;
};
Okay, so we know that thread-safe things can be marked as mutable
, you ask: should they be?
I think we have to consider both view simultaneously. From Herb's new point of view, yes. They are thread safe so do not need to be bound by the const-ness of the function. But just because they can safely be excused from the constraints of const
doesn't mean they have to be. I still need to consider: would it be an error in implementation if I did modify that member? If so, it needs to not be mutable
!
There's a granularity issue here: some functions may need to modify the would-be mutable
member while others don't. This is like wanting only some functions to have friend-like access, but we can only friend the entire class. (It's a language design issue.)
In this case, you should err on the side of mutable
.
Herb spoke just slightly too loosely when he gave a const_cast
example an declared it safe. Consider:
struct foo
{
void act() const
{
const_cast<unsigned&>(counter)++;
}
unsigned counter;
};
This is safe under most circumstances, except when the foo
object itself is const
:
foo x;
x.act(); // okay
const foo y;
y.act(); // UB!
This is covered elsewhere on SO, but const foo
, implies the counter
member is also const
, and modifying a const
object is undefined behavior.
This is why you should err on the side of mutable
: const_cast
does not quite give you the same guarantees. Had counter
been marked mutable
, it wouldn't have been a const
object.
Okay, so if we need it mutable
in one spot we need it everywhere, and we just need to be careful in the cases where we don't. Surely this means all thread-safe members should be marked mutable
then?
Well no, because not all thread-safe members are there for internal synchronization. The most trivial example is some sort of wrapper class (not always best practice but they exist):
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;
};
Here we are wrapping threadsafe_container
and providing another member function we want (would be better as a free function in practice). No need for mutable
here, the correctness from the old point of view utterly trumps: in one function I'm modifying the container and that's okay because I didn't say I wouldn't (omitting const
), and in the other I'm not modifying the container and ensure I'm keeping that promise (omitting mutable
).
I think Herb is arguing the most cases where we'd use mutable
we're also using some sort of internal (thread-safe) synchronization object, and I agree. Ergo his point of view works most of the time. But there exist cases where I simply happen to have a thread-safe object and merely treat it as yet another member; in this case we fall back on the old and fundamental use of const
.
回答2:
I just watched the talk, and I do not entirely agree with what Herb Sutter is saying.
If I understand correctly, his argument is as follows:
[res.on.data.races]/3
imposes a requirement on types that are used with the standard library -- non-const member functions must be thread-safe.Therefore
const
is equivalent to thread-safe.And if
const
is equivalent to thread-safe, themutable
must be equivalent to "trust me, even the non-const members of this variable are thread-safe".
In my opinion, all three parts of this argument are flawed (and the second part is critically flawed).
The problem with 1
is that [res.on.data.races]
gives requirements for types in the standard library, not types to be used with the standard library. That said, I think it is reasonable (but not entirely clear-cut) to interpret [res.on.data.races]
as also giving requirements for types to be used with the standard library, because it would be practically impossible for a library implementation to uphold the requirement to not modify objects through const
references if const
member functions were able to modify objects.
The critical problem with 2
is that while it is true (if we accept 1
) that const
must imply thread-safe, it is not true that thread-safe implies const
, and so the two are not equivalent. const
still implies "logically immutable", it is just that the scope for "logically immutability" has expanded to require thread-safety.
If we take const
and thread-safe to be equivalent, we lose the nice feature of const
which is that it allows us to easily reason about code by seeing where values can be modified:
//`a` is `const` because `const` and thread-safe are equivalent.
//Does this function modify a?
void foo(std::atomic<int> const& a);
Furthermore, the relevant section of [res.on.data.races]
talks about "modifies", which can be reasonably interpreted in the more general sense of "changes in an externally observable way", rather than just "changes in a thread-unsafe way".
The problem with 3
is simply that it can only be true if 2
is true, and 2
is critically flawed.
So to apply this to your question -- no, you should not make every internally synchronised object mutable
.
In C++11, as in C++03, `const` means "logically immutable" and `mutable` means "can change, but the change will not be externally observable". The only difference is that in C++11, "logically immutable" has been expanded to include "thread-safe".
You should reserve mutable
for member variables that do not affect the externally visible state of the object. On the other hand (and this is the key point that Herb Sutter makes in his talk), if you have a member that is mutable for some reason, that member must be internally synchronised, otherwise you risk making const
not imply thread-safe, and this would cause undefined behaviour with the standard library.
回答3:
Let's talk about the change in const
.
void somefunc(Foo&);
void somefunc(const Foo&);
In C++03 and before, the const
version, compared to the non-const
one, provides additional guarantees to the callers. It promises not to modify its argument, where by modification we mean calling Foo
's non-const member functions (including assignment etc), or passing it to functions that expect a non-const
argument, or doing same to its exposed non-mutable data members. somefunc
restricts itself to const
operations on Foo
. And the additional guarantee is totally one-sided. Neither the caller nor the Foo
provider do not have to do anything special in order to call the const
version. Anyone who is able to call the non-const
version can call the const
version too.
In C++11 this changes. The const
version still provides the same guarantee to the caller, but now it comes with a price. The provider of Foo
must make sure that all const
operations are thread safe. Or it least it must do so when somefunc
is a standard library function. Why? Because the standard library may parallelize its operations, and it will call const
operations on anything and everything without any additional synchronization. So you, the user, must make sure this additional synchronization is not needed. Of course this is not a problem in most cases, as most classes have no mutable members and most const
operations don't touch global data.
So what mutable
means now? It's the same as before! Namely, this data is non-const, but it is an implementation detail, I promise it does not affect the observable behavior. This means that no, you don't have to mark everything in sight mutable
, just as you didn't do it in C++98. So when you should mark a data member mutable
? Just like in C++98, when you need to call its non-const
operations from a const
method, and you can guarantee it won't break anything. To reiterate:
- if your data member's physical state does not affect the observable state of the object
- and it is thread-safe (internally synchronized)
- then you can (if you need to!) go ahead and declare it
mutable
.
The first condition is imposed, like in C++98, because other code, including the standard library, may call your const
methods and nobody should observe any changes resulting from such calls. The second condition is there, and this is what's new in C++11, because such calls can be made asynchronously.
回答4:
The accepted answer covers the question, but it's worth mentioning that Sutter has since changed the slide that incorrectly suggested that const == mutable == thread-safe. The blog post that lead to that slide change can be found here:
What Sutter got wrong about Const in C++11
TL:DR Const and Mutable both imply Thread-safe, but have different meanings in regards to what can and can't be changed in your program.
来源:https://stackoverflow.com/questions/14131572/always-declare-stdmutex-as-mutable-in-c11