Mutable

The mutable keyword seems to be one of the less known corners of C++. Yet it can be very useful, or even unavoidable if you want to write const-correct code or lambdas that change their state.

A few days ago, a discussion sparked on Twitter following this Tweet:

The main sentiment of the responses was twofold: Having that kind of questions in interviews is of limited use or no use at all – and I agree. But this post is about the second point many raised, being that mutable is unknown to most and rightfully so. And I disagree with that. mutable can be very useful in certain scenarios.

Const-correctness: semantic const vs. syntactic const

When we strive to write const-correct code, we will come across problems when semantic immutability does not equal syntactic immutability. In other words, we may have to mutate state that is an implementation detail, while the visible state of our object remains the same.

The alteration of internal state is an implementation detail that should not be visible to clients of our class. Yet if we declare a method const, the compiler won’t allow us to change members even if that change is not visible on the outside.

Cached data

A common example is caching of data. Let’s have a look at a polygon class:

class Polygon {
  std::vector<Vertex> vertices;
public:
  Polygon(std::vector<Vertex> vxs = {}) 
    : vertices(std::move(vxs)) 
  {}

  double area() const {
    return geometry::calculateArea(vertices);
  }

  void add(Vertex const& vertex) {
    vertices.push_back(vertex);
  }

  //...
};

Let’s assume that geometry::calculateArea is a slightly costly library function that we do not want to call every time the function is called. We could calculate the area whenever we change the polygon, but that can be equally costly. The typical solution will be to calculate the value only when it’s needed, cache it and reset it when the polygon changes.

class Polygon {
  std::vector<Vertex> vertices;
  double cachedArea{0};
public:
  //...

  double area() const {
    if (cachedArea == 0) {
      cachedArea = geometry::calculateArea(vertices);
    }
    return cachedArea;
  }

  void resetCache() {
    cachedArea = 0;
  }

  void add(Vertex const& vertex) {
    resetCache();
    vertices.push_back(vertex);
  }

  //...
};

The compiler will not let us get away with this because we try to modify cachedArea inside a const function. On the other hand, area is a simple getter function that should be const, since it does not modify the visible state of the object.

Mutexes

Another example is thread safety through mutexes. The vertices container in the example above is not thread safe. So, in a multithreaded application where threads share polygons, we might want to protect the data with mutexes:

class Polygon {
  std::vector<Vertex> vertices;
  std::mutex mutex;
public:
  Polygon(std::vector<Vertex> vxs = {}) 
    : vertices(std::move(vxs)) 
  {}

  double area() const {
    std::scoped_lock lock{mutex};
    return geometry::calculateArea(vertices);
  }

  void add(Vertex const& vertex) {
    std::scoped_lock lock{mutex};
    vertices.push_back(vertex);
  }

  //...
};

Here, the compiler will again complain about area, issuing a lengthy explanation that passing a const mutex to a scoped_lock tries to call mutex::lock which discards qualifiers. In other words: We can’t lock a const mutex.

(In case you were wondering about the missing template arguments of scoped_lock: with C++17 we got class template argument deduction.)

Again, it seems we can not make the method const only because of an implementation detail that has nothing to do with the visible state of our object

mutable to the rescue

The mutable keyword is in the language to address exactly this kind of problem. It is added to member variables to explicitly say “this variable may change in a const context”. With mutable, the combined solution to our two examples above would look like this:

class Polygon {
  std::vector<Vertex> vertices;
  mutable double cachedArea{0};
  mutable std::mutex mutex;
public:
  //...

  double area() const {
    auto area = cachedArea; //read only once
    if (area == 0) {
      std::scoped_lock lock{mutex};
      area = geometry::calculateArea(vertices);
      cachedArea = area;
    }
    return area;
  }

  void resetCache() {
    assert(!mutex.try_lock());
    cachedArea = 0;
  }

  void add(Vertex const& vertex) {
    std::scoped_lock lock{mutex};
    resetCache();
    vertices.push_back(vertex);
  }

  //...
};

Mutable can be applied to any class members that are not references or declared const.

Mutable lambdas

There is one other use for the mutable keyword, and it is for lambdas with state. Usually, the function call operator of a closure (i.e. of the lambda object) is const. Therefore, a lambda cannot modify any members captured by value:

int main() {
  int i = 2;
  auto ok = [&i](){ ++i; }; //OK, i captured by reference
  auto err = [i](){ ++i; }; //ERROR: trying to modify the internal copy of i
  auto err2 = [x{22}](){ ++x; }; //ERROR: trying to modify the internal variable x
}

Here, the mutable keyword can be applied to the lambda itself to make all members mutable:

int main() {
  int i = 2;
  auto ok = [i, x{22}]() mutable { i++; x+=i; };
}

Note, that other than mutable member variables, a mutable lambda should be a rare thing. Transporting state that changes between invocations of a lambda can be very subtle and counterintuitive.

Conclusion

mutable is not a dark and dusty corner of the language you only need to know if you implement compilers or really nasty code to chastise yourself. Instead, it is a tool that goes hand in hand with const, although it used less often. It enables us to get the compiler to help us write safer and more reliable const-correct code.

Previous Post
Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

11 Comments


  1. Hi Arne! First of all, thank you for the nice read, I quite enjoyed it.

    I had a small question regarding the thread safety of the example of Polygon with the std::scoped_lock and the mutex. And more precisely with the area() method.

    Is it okay for the test m_cachedArea == 0 to be outside of the scoped_lock? Or is it a typo? If it is okay, could you explain why it is enough of a check?

    Reply

    1. Hi thanks for questioning that piece of code. It can be ok, but not as I wrote it. Two parallel threads entering the method might both read 0 and trigger the calculation. That is a slight performance hit but might be better than locking in every call of the getter.
      However, after the first non-0 read of m_cachedArea in the if, another thread might reset it to 0, so the second read for the return might yield 0.
      So, instead of reading twice I should have read into a local variable and return that if it’s not 0.
      Will fix asap

      Reply

      1. Proper style also suggests adding a huge scary comment to resetCache indicating that it must be called with the mutex held.

        Reply

        1. I think the saner idea would be to have a public resetCache, which aquires the mutex and a private overload, which requires a lock as the first argument.
          This way you can either reuse the lock or safely call it.

          Reply

      2. In area() you still can read cachedArea while another thread holds the mutex and writes to it, so you still have a data race…

        Reply

        1. But reading it once is ok, since a write is atomic isn’t it?

          Reply

          1. We are talking about conflicting evaluation (see http://en.cppreference.com/w/cpp/language/memory_model).
            Strictly speaking you have a data race, unless you use atomic operations (as in std::atomic), or there is a clear happens-before relationship.
            You don’t do either: cachedArea is a double, not std::atomic, and you don’t lock the mutex for reading, so there are no ordering between the two evaluation.


          2. Why do You think the write is atomic? I am pretty sure, that a thread sanitizer will alert, when area() is executed by different threads. On http://coliru.stacked-crooked.com/a/ce29c0dd4c0896bf You find a similar example. I compiled and run it on my Ubuntu 14.04 (x86_64)…

            clang++-3.8 -std=c++11 -O2 -g -Wall -pedantic -Werror -fsanitize=thread -pthread test.cpp && ./a.out

            WARNING: ThreadSanitizer: data race (pid=30561)
            Write of size 8 at 0x0000015336c8 by main thread (mutexes: write M6):
            #0 (anonymous namespace)::area() /tmp/test.cpp:20:18 (a.out+0x0000004a6a5c)
            #1 (anonymous namespace)::threadFunc() /tmp/test.cpp:30 (a.out+0x0000004a6a5c)
            #2 main /tmp/test.cpp:37 (a.out+0x0000004a6a5c)

            Previous read of size 8 at 0x0000015336c8 by thread T1:
            #0 (anonymous namespace)::area() /tmp/test.cpp:16:17 (a.out+0x0000004a6b79)
            #1 (anonymous namespace)::threadFunc() /tmp/test.cpp:30 (a.out+0x0000004a6b79)
            #2 void std::_Bind_simple::_M_invoke<>(std::_Index_tuple<>) /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/functional:1699:18 (a.out+0x0000004a7169)
            #3 std::_Bind_simple::operator()() /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/functional:1688 (a.out+0x0000004a7169)
            #4 std::thread::_Impl<std::_Bind_simple >::_M_run() /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/thread:115 (a.out+0x0000004a7169)
            #5 (libstdc++.so.6+0x0000000915af)

            Location is global ‘(anonymous namespace)::cachedArea’ of size 8 at 0x0000015336c8 (a.out+0x0000015336c8)

            Mutex M6 (0x0000015336d0) created at:
            #0 pthread_mutex_lock (a.out+0x00000042fda3)
            #1 __gthread_mutex_lock(pthread_mutex_t*) /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/x86_64-linux-gnu/c++/4.9/bits/gthr-default.h:748:12 (a.out+0x0000004a6a4e)
            #2 std::mutex::lock() /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/mutex:135 (a.out+0x0000004a6a4e)
            #3 std::lock_guard::lock_guard(std::mutex&) /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/mutex:377 (a.out+0x0000004a6a4e)
            #4 (anonymous namespace)::area() /tmp/test.cpp:18 (a.out+0x0000004a6a4e)
            #5 (anonymous namespace)::threadFunc() /tmp/test.cpp:30 (a.out+0x0000004a6a4e)
            #6 main /tmp/test.cpp:37 (a.out+0x0000004a6a4e)

            Thread T1 (tid=30563, running) created by main thread at:
            #0 pthread_create (a.out+0x0000004478e3)
            #1 (libstdc++.so.6+0x0000000916f2)
            #2 main /tmp/test.cpp:36:15 (a.out+0x0000004a69f1)

            SUMMARY: ThreadSanitizer: data race /tmp/test.cpp:20:18 in (anonymous namespace)::area()

            finished
            ThreadSanitizer: reported 1 warnings

            Better lock the whole function scope!


  2. If I would add one breaking change to C++, it would be to inverse to usage of const\mutable as keywords. In other words, make every variable\parameter\member function const by default and require the mutable keyword for mutable data.

    Of course, it wouldn’t be a realistic change, but I’m glad they inversed it for lambda functions 🙂

    Reply

  3. I first learned about the usage of mutable here just yesterday.
    She also mentions the cache case.

    Thanks for providing even more examples!

    Reply

    1. I still have to watch that one, thanks for reminding me!

      Reply

Leave a Reply