on
Fifty Shades of Const
As people sometimes say, you’d have to be a bit masochistic to use C++. Well, today I’d like to talk about the “fifty shades” of C++ constants (In case you were wondering, I have most definitely not read the book Fifty Shades of Grey but I’ll plead the fifth on the movie).
I had used C++ a few years back at work but haven’t actually been catching up on this newer standards and features until recently.
The Vanilla Side of Const
If you wrote C programs before, you probably used macros.
// This ANSWER_TO_UNIV is hereby declared immutable
#define ANSWER_TO_UNIV 42
That’s basically the idea of a contract. Back when I started learning programming a long time ago (Yup, maybe I am an old-fashioned engineer after all), the idea of contract-based programming was imprinted on me. To a point that, basically when you write a new function, you should do1:
// Add: adds two integers using a secret formula
// Input: two integers x and y
// Output: The sum of x and y
// Side Effect: Result may overflow upon large input integers
// TODO: Either switch to int64_t or check and return error
int Add(int x, int y)
{
return x + y;
}
Similarly, a constant defined by the macro could be seen as a commitment that’s part of a larger contract. Since C++ inherits from C, you can still use macro to define a constant (there are minor differences tho). But the problem with the ANSWER_TO_UNIV
macro above is: it’s not very helpful in debugging, as the preprocessor will simply do a find-and-replace for ANSWER_TO_UNIV
and change it to 42
for every occurrence, so the compiler actually won’t know anything about this macro at all. That’s why, const
, which is obviously redacted from the English word “constant”, is preferred. A variable with the const
qualifier becomes immutable, but compiler would still know its name.
It’s dead simple to use:
// error. a const must be initialized
const int answer;
// this is OK.
const int answer = 42;
// error. can't assign to a constant
answer = 420;
// error. can't alter a constant
answer *= 10;
The Wild Side of Const
Usually, C++ compilers don’t care that much about the order of the qualifier keywords. Like static const
and const static
would make no difference to the compiler and you may just get away with either style. But if you mix const
with pointers, that’s when magic happens.
I’ll cite an example by Bjarne Stroustrup, you know, the man who is dubbed as the father of C++:
void foo(char* p)
{
char s[] = "Abc";
const char* pc = s; // pointer to constant
pc[0] = 'B'; // error. pc points to a constant
pc = p; // this is OK
char *const cp = s; // the pointer itself is a constant
cp[0] = 'B'; // this is OK
cp = p; // error. can't modify a const pointer
// Can we use two const to declare? Sure thing, why not?
const char *const cpc = s;
cpc[0] = 'B'; // no can't do. cpc points to a constant
cpc = p; // can't do this either. cpc itself is also a constant
}
That’s why const
with a raw pointer (or an array) always warrants more attention as it’s very error-prone. It’s confusing to us programmers sometimes cuz putting const
first is more human-readable and thus is more tempting - well, it turns out sometimes it doesn’t matter to compilers but other times it does!
However, that’s not all const
is capable of. It can also serve as a witness for a contract of not changing the status quo.
In this example, function print
will print out the value of a given string s
. However, with the const
qualifier for the input parameter, it also “promises” not to change the value of s
in the process (as it shouldn’t be), especially when the input is a reference that permits potential alteration of its value:
void print(const string& s)
{
std::cout << "string value is: " << s << '\n';
}
Another example could be a const
being used in a class member method, as a “promise” that the operation won’t change anything of the object instance.
class Car
{
public:
Car(int wheels);
int GetWheels() const;
private:
int m_wheels;
};
// Simply reading something. A getter should not be changing anything
int Car::GetWheels() const
{
return m_wheels;
}
The constexpr Specifier
So, as aforementioned, macros can be used to define a constant, but only during pre-processing time, which is before compilation. const
however, will handle this at compilation time or later at runtime.
int GetAnswer()
{
return 42;
}
// Good. Loud and clear. Compiler knows it already at compilation time
const int answer = 42;
// While this is allowed, this constant won't get initialized during compilation time,
// cuz GetAnswer() will return when run-time
const int answer2 = GetAnswer();
So what if you want a constant that’s initialized at compilation time when possible? Introducing constexpr
since C++ 11. Here’s the difference:
// this is OK.
constexpr int answer = 42;
// error. Can't use runtime initialization here.
constexpr int answer2 = GetAnswer();
Besides variables, constexpr
can also be used for functions and constructors, since its name obviously derives from “constant expression”. There could well be other nuances of constexpr
vs const
, but TBH I haven’t really found a lot scenarios to apply it.. yet.
Takeaways
Well, I guess the key takeaway is, C++ is indeed an incredibly flexible and complicated programming language. To ramble about this, I had to dig out the old C++ textbook again. Without a doubt, it takes an obscene amount of time and experience to catch the many caveats like this, which I’m sure is just a tip of the iceberg, even just for the topic of constants/const
.
Speaking of which, part of me admire the Pythonic zen or Go’s simplistic mindset:
There should be one– and preferably only one –obvious way to do it.
For instance, I recall Go even enforces styles (been a long time since I wrote Go too). For programming languages that use curly braces, lot of programmers go to holy wars for whether or not left curly brace should occupy its own line. But Go simply ended the discussion at the compiler level:
// Good.
func foo(x int, y int) int {
return x + y
}
// Bad. Someone call the police! Doesn't even compile. Period.
func foo(x int, y int) int
{
return x + y
}
But I get why C++ is so flexible and that it might be an acquired taste. Its rich support for different programming paradigms is a sharp yet double-edged sword; and the handle can be very hard to grasp. Once you do tame it tho, it’ll do proper damages work as you wish. You can definitely write powerful and performant programs with it.
Credit: reddit
To sum it up, I think it’s almost always a good idea to use const
whenever you can in C++ cuz it prevents unintended changes and safeguards potential breaches of your design contracts. Just look at the amazing steps JavaScript took for const
(jk.. js is not even a static language and the whole let
vs const
is just.. no comments! :D)
Further Reading
- Google C++ Style Guide.
- Microsoft also keeps a decent online C++ documentation.
- The C++ Programming Language by Bjarne Stroustrup.
-
Today we have
///
comments from IDEs like Visual Studio to pre-fill fields automatically, but the gist remains the same - the functions oughta be documented properly of what it consumes and produces, with or without any side effects of variables, and throwing exceptions or not, etc. So the caller should just learn how to use it by hovering on the function name and reading the retrieved doc definition. Of course, there are other folks arguing and practicing the philosophy of “good code should document itself”, which is also a valid point especially when your function/method is pretty much self-explanatory and standard doc comments may even be longer than the actual code. ↩