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.
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 complement 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
.