Always declare std::mutex as mutable in C++11?

Philipp Claßen picture Philipp Claßen · Jan 3, 2013 · Viewed 12.8k times · Source

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.

Answer

GManNickG picture GManNickG · Jan 3, 2013

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.