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

痞子三分冷 提交于 2019-12-29 11:45:16

问题


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:

  1. [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.

  2. Therefore const is equivalent to thread-safe.

  3. And if const is equivalent to thread-safe, the mutable 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!