The mutable keyword in C ++
- Transfer
The mutable keyword refers to little-known corners of the C ++ language. At the same time, it can be very useful, or even necessary if you want to strictly adhere to the const-correctness of your code or write lambda functions that can change their state.
A couple of days ago, Eric Smolikowski tweeted:
“I often ask programmers for interviews how well (on a 10-point scale) they know C ++. Usually they answer 8 or 9. And then I ask what is “mutable”. They do not know. :) ”
My impressions of such questions and answers are twofold. On the one hand, asking such questions at an interview is a futile affair; it says almost nothing about the interviewee’s abilities. But, on the other hand, the mutable keyword undeservedly forgotten by many programmers, and yet it can be very useful in some scenarios.
When we try to write code that is correct in terms of using the concept of constancy, we will encounter the fact that semantic immutability is not equivalent to syntactic immutability. In other words, we may need to change the state of the object (if implementation details require it), while keeping the state of the object visible from the outside constant.
A change in the internal state may be required for some deeply technical reasons and this should not be noticeable to external clients of our class. But our choice is not big - if we use the const keyword when declaring a method, then the compiler will not allow us to change the object of this class, even if no one outside the class will notice these changes.
A good example would be data caching. Let's look at this polygon class:
Let's assume that geometry :: calculateArea is a very resource-intensive function that we do not want to call every time the area () method is called. We can calculate the new area when changing the polygon, but in some scenarios it can be as much (or even more) resource-intensive. A good solution in this situation may be to calculate the area only when necessary, with caching the result and clearing the cache if the polygon changes.
But hey, hey, not so fast! The compiler will not let you do this trick, because the area () method is marked constant, and for some reason we are trying to change the cachedArea property in it. Remove const from method declaration? But then clients of this class will not understand us. After all, area () is a simple getter, this function should definitely not change anything in the class. So why is there no const in her declaration?
Another example is thread safety using mutexes. The vertices container in the example above is not thread safe. Thus, in a multi-threaded application, where different streams will share data from the same polygons, we need to ensure access to this data is secure:
In this case, the compiler will again start complaining about the area () method, which vigorously promises to be constant, but itself (that's a scoundrel!) Is trying to perform the mutex :: lock () operation, which changes the state of the mutex. That is - we cannot lock a constant mutex.
It turns out that again we cannot make the area () method constant and will be forced to either abandon thread safety or mislead the clients of our class by getting rid of const in the method declaration. Due to the technical details of the implementation, which have absolutely nothing to do with the state of the object that is visible from the outside, we have to either refuse part of the functionality or mislead class users.
The mutable keyword exists in the C ++ language standard specifically for solving this class of problems. It can be added to class member variables to indicate that a given variable can change even in a constant context. Using mutable, the solution to both of the above examples would look like this:
There is another option for using the mutable keyword and it is associated with state preservation in lambda functions. Normally, the closure function call statement is constant. In other words, a lambda cannot modify variables captured by value:
But the mutable keyword can be applied to the entire lambda function, which makes all its variables mutable:
It should be noted that, unlike mutable variables in a class declaration, mutable lambda functions should be used relatively rarely and very carefully. Saving state between calls to a lambda function can be dangerous and counterintuitive.
mutable is not some dark and dusty corner of the C ++ language that you will never need. This is a tool that plays a role in clean code, and plays it the better, the more often you use const and the more you try to make your code safe and reliable. With mutable, you can better explain to the compiler where its checks are important and necessary, and where you want to avoid them. All this enhances the overall correctness of the code.
A couple of days ago, Eric Smolikowski tweeted:
“I often ask programmers for interviews how well (on a 10-point scale) they know C ++. Usually they answer 8 or 9. And then I ask what is “mutable”. They do not know. :) ”
My impressions of such questions and answers are twofold. On the one hand, asking such questions at an interview is a futile affair; it says almost nothing about the interviewee’s abilities. But, on the other hand, the mutable keyword undeservedly forgotten by many programmers, and yet it can be very useful in some scenarios.
Const-correctness: semantic constancy versus syntactic constancy
When we try to write code that is correct in terms of using the concept of constancy, we will encounter the fact that semantic immutability is not equivalent to syntactic immutability. In other words, we may need to change the state of the object (if implementation details require it), while keeping the state of the object visible from the outside constant.
A change in the internal state may be required for some deeply technical reasons and this should not be noticeable to external clients of our class. But our choice is not big - if we use the const keyword when declaring a method, then the compiler will not allow us to change the object of this class, even if no one outside the class will notice these changes.
Cached data
A good example would be data caching. Let's look at this polygon class:
class Polygon {
std::vector vertices;
public:
Polygon(std::vector 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 very resource-intensive function that we do not want to call every time the area () method is called. We can calculate the new area when changing the polygon, but in some scenarios it can be as much (or even more) resource-intensive. A good solution in this situation may be to calculate the area only when necessary, with caching the result and clearing the cache if the polygon changes.
class Polygon {
std::vector 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);
}
//...
};
But hey, hey, not so fast! The compiler will not let you do this trick, because the area () method is marked constant, and for some reason we are trying to change the cachedArea property in it. Remove const from method declaration? But then clients of this class will not understand us. After all, area () is a simple getter, this function should definitely not change anything in the class. So why is there no const in her declaration?
Mutexes
Another example is thread safety using mutexes. The vertices container in the example above is not thread safe. Thus, in a multi-threaded application, where different streams will share data from the same polygons, we need to ensure access to this data is secure:
class Polygon {
std::vector vertices;
std::mutex mutex;
public:
Polygon(std::vector 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);
}
//...
};
In this case, the compiler will again start complaining about the area () method, which vigorously promises to be constant, but itself (that's a scoundrel!) Is trying to perform the mutex :: lock () operation, which changes the state of the mutex. That is - we cannot lock a constant mutex.
It turns out that again we cannot make the area () method constant and will be forced to either abandon thread safety or mislead the clients of our class by getting rid of const in the method declaration. Due to the technical details of the implementation, which have absolutely nothing to do with the state of the object that is visible from the outside, we have to either refuse part of the functionality or mislead class users.
Keyword "mutable" to the rescue
The mutable keyword exists in the C ++ language standard specifically for solving this class of problems. It can be added to class member variables to indicate that a given variable can change even in a constant context. Using mutable, the solution to both of the above examples would look like this:
class Polygon {
std::vector vertices;
mutable double cachedArea{0};
mutable std::mutex mutex;
public:
//...
double area() const {
auto area = cachedArea;
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 lambda functions
There is another option for using the mutable keyword and it is associated with state preservation in lambda functions. Normally, the closure function call statement is constant. In other words, a lambda cannot modify variables captured by value:
int main() {
int i = 2;
auto ok = [&i](){ ++i; }; //OK, i захватывается по ссылке
auto err = [i](){ ++i; }; //Ошибка: попытка изменения внутренней копии i
auto err2 = [x{22}](){ ++x; }; //Ошибка: попытка изменения внутренней переменной x
}
But the mutable keyword can be applied to the entire lambda function, which makes all its variables mutable:
int main() {
int i = 2;
auto ok = [i, x{22}]() mutable { i++; x+=i; };
}
It should be noted that, unlike mutable variables in a class declaration, mutable lambda functions should be used relatively rarely and very carefully. Saving state between calls to a lambda function can be dangerous and counterintuitive.
conclusions
mutable is not some dark and dusty corner of the C ++ language that you will never need. This is a tool that plays a role in clean code, and plays it the better, the more often you use const and the more you try to make your code safe and reliable. With mutable, you can better explain to the compiler where its checks are important and necessary, and where you want to avoid them. All this enhances the overall correctness of the code.