7

I recently came across Herb Sutter's video from about how the meaning of const and mutable has changed in C++11 to mean bitwise const (and thread-safe, as a consequence) instead of the traditional logically const.

Five years later, have programmers migrated to this new meaning of const?

I've tried using this while writing a new application but it seems that I'm marking every private member as mutable because they're either protected by a mutex or thread-safe (by their implementation) - which doesn't feel right (I'm inexperienced and do not have a basis for this).

Is it good practice while designing a thread-safe application to mark every member function const and every member variable mutable?

Deduplicator
  • 8,591
  • 5
  • 31
  • 50
CK.
  • 187
  • 1
  • 2
  • 1
    `const` objects work well with thread-safety because they are immutable. Even if you share references to the const object, you won't need to have any locking mechanisms to protect accessing the const object. However, mutable objects are where most problems with thread-safety occur. Some languages (like Erlang) go so far as to codify that in the language. C++ `const` members are initialized in the constructor and never changed, which is really what makes it threadsafe. – Berin Loritsch Oct 05 '18 at 20:14
  • I understand that, but my question is, should I mark every function that is thread-safe as `const` just because it is thread-safe? Which implies that I have to mark every member that it changes in a thread-safe way as `mutable`. – CK. Oct 05 '18 at 20:17
  • 1
    @CK.: I don't remember the part of that presentation which says that all things which are thread-safe are, or of a right should be, `const`. It merely says that `const` things are thread-safe. – Nicol Bolas Oct 05 '18 at 20:18
  • @NicolBolas That is true, it doesn't mention a two-way implication. In that case, what is the basis for using `const` now? Is it still "logical" const? – CK. Oct 05 '18 at 20:20
  • 1
    @CK.: The presentation is basically saying that if you make a function `const`, people will expect to be able to call such a function on that object from multiple threads. So if that function would do something where that's not the case, you need to deal with it within the `const` function(s). For most users, their code need not change, since it implicitly does that anyway. – Nicol Bolas Oct 05 '18 at 20:32
  • 1
    The most common member to modify from a constant member-function is probably a mutex, as part of implementing a cache, or for protection against concurrent writers. – Deduplicator Oct 05 '18 at 20:47
  • @NicolBolas: What does it mean for new interfaces? When we're writing a new class, what's the criteria for a `const` function? Do we still think of it in terms of "logical" `const`ness or do we base it upon it's thread safety? – CK. Oct 05 '18 at 20:51
  • 7
    @CK.: You keep trying to look as this as "thread safe iff `const`". It's merely "`const` ought to be thread safe". – Nicol Bolas Oct 05 '18 at 21:58
  • @CK. or to rephrase it, it's perfectly OK to have a thread safe mutating method, because nothing breaks by people assuming it thread unsafe. – Caleth Oct 07 '18 at 22:22

4 Answers4

10

The video you cited is for an advanced and expert audience. Herb Sutter tries to bring this audience to a consensus on how to best communicate the intent of these keywords to other people, in this age of everything yearning to be thread-safe.

So don't expect that this video contains all you need to know to write perfectly thread-safe code.


I'm marking every private member as mutable because they're either protected by a mutex or thread-safe (by their implementation) - which doesn't feel right (I'm inexperienced and do not have a basis for this).

Here is a hypothetical example of what would be blatantly wrong:

class Size
{
    mutable int x;
    mutable int y;
    // ...
public:
    void setSize(int newX, int newY) const // (first scream)
    {
        // modify members "x" and "y", "SAFELY!" (second scream)
    }
};

The function signature screams wrong, because a function named setSize couldn't possibly be const. The implementation of the function's code would eventually require marking the members "x" and "y" as mutable, which would scream wrong too.

It doesn't matter what code is in the setSize function. If the function's name implies that the function will change the state of the object, it should not be marked with const. Let's suppose we remove the const keyword from the function. Now we find that we don't have to use the mutable keyword on the members "x" and "y". Problem solved.

In other words, if the use of const and mutable are both against intuition, and if such awkward usage occur in pairs, then one should to be suspicious of their usage being wrong.

However, the video mentions one exceptional case. If a class contains a std::mutex member, it is almost always correct to mark it as a mutable member. Please refer to the video for explanations.

Once you have some experience doing it correctly, you can rely on your intuition to see whether it looks natural, and natural means correct.

If a function's name implies that it should not change the state of the object, then it should be marked with const, and the code inside needs to be made thread-safe, by not performing thread-unsafe operations.

This is the mentality expressed in the video linked above. When it comes to thread-safety, users of your code (yourself, or your fellow teammates) have certain expectations that certain functions shouldn't do certain things.

The video mentions these examples:

  • bool operator == (const T& other) const should never modify this or other, because nobody would expect comparisons to modify anything. (This is called "side-effect free".)
  • class T { public: T(const T& other) {...} }; should never modify other, because the same instance of other might have been passed into two threads, each threading passing it into the copy constructor.

How about this code?

class Size
{
    const int x;
    const int y;
    // ...
public:
    Size(int init_x, int init_y)
        : x(init_x), y(init_y)
    {
    }
};

This code is even better. It demonstrates the correct use of const on members.

However, it does prevent one from using the Size class in certain ways. For example, you cannot use assignment operators to overwrite an existing instance of Size. In other words, following the best practice when implementing the Size class, may require you to redesign other source code that uses Size. This is the "contagious issue" that makes const-ness an important consideration for a C++ project.

Robert Harvey
  • 198,589
  • 55
  • 464
  • 673
rwong
  • 16,695
  • 3
  • 33
  • 81
  • 2
    There is a well-known example of a copy-ctor which cannot avoid changing an object's state: `std::shared_ptr`. – Deduplicator Oct 06 '18 at 18:58
  • 1
    @Deduplicator conceptually, it's in the same category as `std::mutex` members. Pedantically, there's no modification to the *pointer-to-control-block* member of the `std::shared_ptr` – Caleth Oct 07 '18 at 15:10
  • What do you mean exactly by making a `const` function _thread-safe_? Even reading a data member may be a thread-unsafe action, if another thread may write to it. Please see the following question for more details: [What is the definition of a thread safe function?](https://stackoverflow.com/questions/67143880/what-is-the-definition-of-a-thread-safe-function-according-to-the-c11-languag) – m7913d Apr 21 '21 at 17:09
  • @m7913d My answer is first and foremost a response to OP's question about Herb Sutter's video. It may not fully cover other aspects. From my own experience, multithreading code running on ARM with shared data will require additional safeguards, due to differences in memory ordering guarantees between ARM and x86 architectures. My original answer already had a disclaimer, with edits by Robert Harvey. I would refer readers to [this blog article by Bruce Dawson (randomascii.wordpress.com)](https://randomascii.wordpress.com/2020/11/29/arm-and-lock-free-programming/) for more details. – rwong Apr 22 '21 at 14:40
  • 1
    @rwong, I really think we (including Herb Sutter) should not use the term `thread-safe` in this case, but rather bitwise const or internally synchronised. I added a new answer to explain this point of view. – m7913d Apr 22 '21 at 16:02
7

Is it good practice while designing a thread-safe application to mark every member function const and every member variable mutable?

No, that is not good practice.

A const member function signals that the function will not modify the object it is called upon. Because the object won't be modified, it is safe to call the function from multiple thread without external locking.

However, it is not the case that const member functions are the only thread-safe functions. Any other function can be thread safe as well.
Using the const modifier to mark a mutating function as thread-safe is giving the wrong signal.

Bart van Ingen Schenau
  • 71,712
  • 20
  • 110
  • 179
  • Thank you for the answer. "Using the const modifier to mark a mutating function as thread-safe is giving the wrong signal." So the driving rule for marking a function `const` is still "if it's logically const". – CK. Oct 08 '18 at 13:46
  • Yes. The answer by @rwong explains it better that I did. – Bart van Ingen Schenau Oct 08 '18 at 15:41
  • What do you mean exactly by a `const` function that is _thread-safe_? Even reading a data member may be a thread-unsafe action, if another thread may write to it. Please see the following question for more details: [What is the definition of a thread safe function?](https://stackoverflow.com/questions/67143880/what-is-the-definition-of-a-thread-safe-function-according-to-the-c11-languag) – m7913d Apr 21 '21 at 17:15
1

As already stated by others, a const function implies it should be thread-safe to call it from multiple threads simultaneously, without calling a non const function at the same time. As Herb Sutter (29:43) stated himself, this means that a const function should be bitwise const or internally synchronised, which isn't really thread-safe if other non-const functions may be called at the same time.

Thread-safe definitely does not mean/imply const.

Note that the following old threat is still relevant: Does const mean thread-safe in C++11?, but bear in mind that the term thread-safe is often abused (see What is the definition of a thread safe function according to the C++11 (Language/Library) Standard?), i.e. in this context thread-safe is used for bitwise const or internally synchronised.

m7913d
  • 111
  • 3
  • I'm still curious and a bit confused. May I ask you a question - Suppose the function/thread/core that writes data (e.g. constructs a heap-allocated object) issues a release barrier, and each function/thread/core that reads that same object issues an acquire barrier when it senses that it's the first time that the thread/core encounters that object. Would this be sufficient for solving the memory consistency issue on ARM platforms? I've found plenty of discussions online; this seems to be what most people prescribe, yet I've seen mixed opinions as to whether it's fail-proof. – rwong Apr 22 '21 at 17:54
  • 1
    Is this answer a reference to the concept of "shared mutability"? That is, when an object is used by multiple cores, and if at least one such core performs a write operation, then every core involved will be potentially exposed to the issue of shared mutability. Within this context, bitwise const-ness is a property that is communicated from the source code (declaration) to the programmer writing code that uses it (calling the functions) that this function isn't a "write" function. It is the programmer's responsibility to ensure that nobody is calling a function that "writes" when it's read. – rwong Apr 22 '21 at 18:38
  • @rwong, I'm not familiar with ARM platforms, especially not in case of lock free programming. – m7913d Apr 23 '21 at 07:46
  • @rwong, according to your description of shared mutability, I agree that's the concept I'm referring too. (Note that others seems to reserve the term for [concurrent writes](https://2ality.com/2019/10/shared-mutable-state.html#what-is-shared-mutable-state-and-why-is-it-problematic%3F)) – m7913d Apr 23 '21 at 07:56
0

The meaning of the const keyword is still 'logically const': it's just that logical constness now has to take account of thread safety as well as other things.

All 'logically const' means is that, from the point of view of the public interface of the class, we appear to be a const function. This is a good concept, because the public interface of the class is the only thing that should make a difference to the rest of the program. Our const function might store some information to a cache, or change the bitwise state in some other way, but that's an implementation detail: if external code can't tell that it's not bitwise const, then it should be marked const - possibly with some members marked mutable to make this possible.

With C++11 and its multithreading capabilities, this definition becomes more complicated. If a function is truly bitwise const, then it is automatically safe to call it from multiple threads concurrently. This means that any function that is not thread safe is automatically not logically const - because it behaves differently from a bitwise const function from the point of view of external code. A bitwise const function would be thread safe.

So while it's true that you should make sure that any function that is marked const in your code is thread safe, the reverse does not apply: it's perfectly legal to have functions that are thread safe but are not logically const for other reasons.

That said, if your code calls a non-const function, then that code should assume that the function is not thread-safe: if it needs to be called concurrently, then the individual calls should be protected by mutexes in the enclosing scope. It may be the case that the function is thread-safe at the moment, but that is an implementation detail and can be changed without changing the signature of the function.

On the other hand, if the function is const and you change it to be non-thread-safe, then you have broken the contract of the function.

John Gowers
  • 153
  • 5
  • To recap: You say that any logically-const function must be thread-safe. I contend that while logically-const functions which are not bit-const (immutable) and thus thread-safe are rare, they are important. It's just that the C++ type system doesn't really know `immutable`. – Deduplicator Apr 07 '20 at 12:51
  • What do you mean exactly by a `const` function that is _thread-safe_? Even reading a data member may be a thread-unsafe action, if another thread may write to it. Please see the following question for more details: [What is the definition of a thread safe function?](https://stackoverflow.com/questions/67143880/what-is-the-definition-of-a-thread-safe-function-according-to-the-c11-languag) – m7913d Apr 21 '21 at 17:16
  • If another thread may write to the data member, then it is the other thread that is exhibiting non-thread safe behaviour, not the `const` function. – John Gowers Apr 21 '21 at 17:37