Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In what sense const allows only atomic changes to mutable member variables?

I'm reading Functional Programming in C++ from Ivan Čukić, and I am having a hard time interpreting a point in the summary of Chapter 5:

  • When you make a member function const, you promise that the function won't change any data in the class (not a bit of the object will change), or that any changes to the object (to members declared as mutable) will be atomic as far as the users of the object are concerned.

If the part in italic was simply are limited to members declared as mutable I would have been happy with it. However, this rewording of mine seems to correspond to what the author put in parenthesis. What is out of parenthesis is what is puzzling me: what is the meaning of atomic in that sentence?

like image 455
Enlico Avatar asked Dec 22 '22 18:12

Enlico


2 Answers

The author is making a claim about best practices, not about the rules of the language.

You can write a class in which const methods alter mutable members in ways that are visible to the user, like this:

struct S {
    mutable int value = 0;
    int get() const {
        return value++;
    }
};
const S s;
std::cout << s.get();  // prints 0
std::cout << s.get();  // prints 1
// etc

You can do that, and it wouldn't break any of the rules of the language. However, you shouldn't. It violates the user's expectation that the const method should not change the internal state in an observable way.

There are legitimate uses for mutable members, such as memoization that can speed up subsequent executions of a const member function.

The author suggests that, as a matter of best practices, such uses of mutable members by const member functions should be atomic, since users are likely to expect that two different threads can call const member functions on an object concurrently.

If you violate this guideline, then you're not directly breaking any rules of the language. However, it makes it likely that users will use your class in a way that will cause data races (which are undefined behaviour). It takes away the user's ability to use the const qualifier to reason about the thread-safety of your class.

like image 60
Brian Bi Avatar answered May 11 '23 00:05

Brian Bi


or that any changes to the object (to members declared as mutable) will be atomic as far as the users of the object are concerned.

I think the author (or editor) of the book worded his statement poorly there -- const and mutable make no guarantees about thread-safety; indeed, they were part of the language back when the language had no support for multithreading (i.e. back when multithreading specifications were not part of the C++ standard and therefore anything you did with multithreading in your C++ program was therefore technically undefined behavior).

I think what the author intended to convey is that changes to mutable member-variables from with a const-tagged method should be limited only to changes that don't change the object's state as far as the calling code can tell. The classic example of this would be memo-ization of an expensive computation for future reference, e.g.:

class ExpensiveResultGenerator
{
public:
    ExpensiveResultGenerator()
       : _cachedInputValue(-1)
    {
    }

    float CalculateResult(int inputValue) const
    {
       if ((_cachedInputValue < 0)||(_cachedInputValue != inputValue))
       {
          _cachedInputValue = inputValue;
          _cachedResult     = ReallyCPUExpensiveCalculation(inputValue);
       }
       return _cachedResult;
    }

private:
    float ReallyCPUExpensiveCalculation(int inputValue) const
    {
        // Code that is really expensive to calculate the value
        // corresponding to (inputValue) goes here....
        [...]
        return computedResult;
    }

    mutable int _cachedInputValue;
    mutable float _cachedResult;
}

Note that as far as the code using the ExpensiveResultGenerator class is concerned, CalculateResult(int) const doesn't change the state of the ExpensiveResultGenerator object; it is simply computing a mathematical function and returning the result. But internally we are making a memo-ization optimization so that if the user calls CalculateResult(x) with the same value for x multiple times in a row, we can skip the expensive calculation after the first time and just return the _cachedResult instead, for a speedup.

Of course, making that memo-ization optimization can introduce race conditions in a multi-threaded environment, since now we are changing state variables even if the calling code can't see us doing it. So to do this safely in a multithreaded environment, you would need to employ a Mutex of some sort to serialize accesses to the two mutable variables -- either that, or require the calling code to serialize any calls to CalculateResult().

like image 24
Jeremy Friesner Avatar answered May 11 '23 00:05

Jeremy Friesner