Modern C++
Don't Cast Away Const
Too few C++ developers use const properly, or enough. A seemingly-strange guideline, suggesting you never use a particular language feature, leads to some insight about const and some good practices for you.
- By Kate Gregory
- 04/26/2016
In this series, I'm exploring a handful of guidelines from the C++ Core Guidelines project. You can always find the latest version on GitHub -- look at the CppCoreGuidelines.md file. There are hundreds of guidelines in the document; some are common sense and have always been a good practice, while others come as a surprise to some experienced C++ programmers. Many rely on relatively new language features. By exploring a particular guideline, I can exercise a few language features that not all C++ developers know. In this article, I want to make const a little easier to work with.
So, check out Guideline ES.50, which says, "Don't cast away const."
Talk about short and sweet! There's no "prefer" or "avoid" here, just a flat-out forbidding. It goes on to provide a Reason: It makes a lie out of const. The Note elaborates:
Usually the reason to "cast away const" is to allow the updating of some transient information of an otherwise immutable object. Examples are caching, memoization, and precomputation. Such examples are often handled as well or better using mutable or an indirection than with a const_cast.
And as I write this article, the example hasn't yet been provided. So I thought I'd provide one.
First, let's take a small detour for the word memoization. It's a subset of caching. You might cache any number of things for any number of reasons. Maybe you want to improve performance of this app, or you might want to reduce your bandwidth consumption, reduce pressure on some other hardware or software (such as a database server), lower the chance of contention between two other processes, or help a hard drive last longer. Sometimes, cached values are always correct, because you invalidate the cache whenever you do something that means the value isn't right any more; but other times, the cached value might be incorrect, and you just hope it's not incorrect by very much. You invalidate caches based on time, or how much space they're filling up, or any other approach that works in your situation.
Memoization is caching something specifically for performance reasons, and specifically knowing that the cached value is always correct. Some people go so far as to say it's caching the result of a function call and not, for example, an expression. But you need to know deterministically that you would be getting the same result if you were to make the call instead of using the cache.
Const correctness is a really big deal to most C++ developers. Some, like me, know that it can reduce bugs and improve performance, and we're pretty adamant about marking everything const that we possibly can. Others aren't sure what all the fuss is about, and spend a fair amount of time fighting the compiler when working in a code base that is partially const-correct. Making a small change can cause a lot of misery, sometimes so much that the developer gives up on const-correctness, which is a real shame. Other times, they use far-too-powerful approaches, including casting away const, to get out of some trouble they find themselves in.
Imagine a class like this:
class Stuff
{
private:
int number1;
double number2;
int LongComplicatedCalculation() const;
public:
Stuff(int n1, double n2) : number1(n1), number2(n2) {}
bool Service1(int x);
bool Service2(int y);
int getValue() const;
};
Without knowing what any of this is, you can see that getValue doesn't change any member variables, and neither does LongComplicatedCalculation. You can also guess quite strongly that Service1 and Service2 do change member variables, because this developer didn't mark them const, and probably would have if it was possible. Now imagine I show you that getValue looks like this:
int Stuff::getValue() const
{
return LongComplicatedCalculation();
}
No surprise there. But imagine if LongComplicatedCalculation really is long and complicated -- you might not want to be calling it over and over if you don't have to. It could improve performance to cache the result and use that as long as it's still valid. Because LongComplicatedCalculation doesn't take any parameters, it can only depend on the member variables of the class. One simple approach is to add a member variable for the result, and calculate the result at the end of those functions that change member variables. The class might then look like Listing 1.
Listing 1: LongComplicatedCalculation
class Stuff
{
private:
int number1;
double number2;
int LongComplicatedCalculation() const;
int cachedValue;
public:
Stuff(int n1, double n2) : number1(n1), number2(n2), cachedValue(0) {}
bool Service1(int x);
bool Service2(int y);
int getValue() const;
};
And the member functions like Listing 2.
Listing 2: Member Functions
bool Stuff::Service1(int x)
{
number1 = x;
// Other real calculations
cachedValue = LongComplicatedCalculation();
return true;
}
bool Stuff::Service2(int y)
{
number2 = y / 3.0;
// Other real calculations
cachedValue = LongComplicatedCalculation();
return true;
}
int Stuff::getValue() const
{
return cachedValue;
}
Terrific. We've improved performance, and maintained const-correctness. All is good. But imagine a performance problem remains. The caching pattern here assumes that we get the value far more often than we change the member variables. This is a good assumption for things like your bank balance or what day it is today. But it doesn't hold universally -- people throughout a company might update their timesheets many times a day with a journaling app, but management might only look at the totals once or twice a week, for example. Your net worth fluctuates every time you spend or earn any money, but you might only calculate it every few months. So what happens if our toy program wants to change the caching pattern, and only calculate that value when someone wants it, and it's changed since last time?
You can add a flag to indicate whether the cache is valid, and set the flag to false in the functions that change the member variables. In getValue, if the flag is true, just return the cached value, otherwise, update it, as shown in Listing 3.
Listing 3: Set Flags in GetValue()
bool Stuff::Service1(int x)
{
number1 = x;
// Other real calculations
cacheValid = false;
return true;
}
bool Stuff::Service2(int y)
{
number2 = y / 3.0;
// Other real calculations
cacheValid = false;
return true;
}
int Stuff::getValue() const
{
if (!cacheValid)
{
cachedValue = LongComplicatedCalculation();
cacheValid = true;
}
return cachedValue;
}
This looks great, but there's one problem: It won't compile. It's changing member variables (cachedValue and cacheValid), but it's marked const. Some people would react by removing the const notation from getValue. Please don't do that! As I said before, const-correctness is really valuable and you don't want to give it away. There's a good chance you'll break some other code that calls this function -- and will you go through the code removing more and more const annotations until the thing finally compiles? That's a horrible approach. So what can you do?
Well, you could do what the guideline says not to -- cast away const, like this:
int Stuff::getValue() const
{
if (!cacheValid)
{
Stuff* This = const_cast<Stuff*>(this);
This->cachedValue = LongComplicatedCalculation();
This->cacheValid = true;
}
return cachedValue;
}
You're creating a non-const pointer called This and pointing it at the instance for which getValue was called, then using the non-const pointer to change the values of the member variables. This will compile, but it's awful. Once you've made this cast, you can do whatever you like with that This pointer. You could change any member variables you want:
This->number1 = -1;
That line will compile just fine. Casting away const gives you way too much power. It also makes the header file deceptive -- anyone reading it will believe getValue doesn't change any member variables, even though it does.
A much better approach is to tell the truth in your header file, by applying the keyword mutable correctly, as shown in Listing 4.
Listing 4: Tell the Truth, Apply Mutable
class Stuff
{
private:
int number1;
double number2;
int LongComplicatedCalculation() const;
mutable int cachedValue;
mutable bool cacheValid;
public:
Stuff(int n1, double n2) : number1(n1), number2(n2),cachedValue(0),cacheValid(false) {}
bool Service1(int x);
bool Service2(int y);
int getValue() const;
};
Now getValue can change cachedValue and cacheValid, but only those two variables, nothing else. Anyone reading this header file knows that const on a function doesn't keep that function from changing the cache variables, but does keep it from changing number1 and number2.
By following the guideline and not casting away const, you've added real value to your header file. You've also kept getValue shorter (because it can go back to the version without the const_cast that just changed cachedValue and cacheValid), easier to write and test, and easier to understand. Like the other guidelines, this one will help you to write code that is easier to write, read and maintain while also being faster, because const-correctness helps optimizers speed up your code. It's not a matter of trade-offs -- it's a matter of doing better on all measurements at once. Who wouldn't want that?
About the Author
Kate Gregory has been using C++ since before Microsoft had a C++ compiler, and has been paid to program since 1979. Kate runs a small consulting firm in rural Ontario, Canada, and provides mentoring and management consultant services, as well as writing code every week. She has spoken all over the world, written over a dozen books, and helped thousands of developers to be better at what they do. Kate is a Microsoft Regional Director, a Visual C++ MVP, an Imagine Cup judge and mentor, and an active contributor to StackOverflow and StackExchange sites. She develops courses for Pluralsight, primarily on C++ and Visual Studio. Since its founding in 2014 she is the Open Content Chair and speaker for CppCon, the definitive C++ conference.