3.6. Keyword: const
¶
Use const to instruct the compiler to hold something constant.
The const
keyword can modify the behavior of anything, depending on where it is used:
fundamental types
structs or classes
functions and function parameters
pointers and references
or the values stored in pointers and references
Programs typically use a lot of constants. Mathematical constants like \(\pi\), or \(e\), and unit conversions are common. Obviously, it would be bad if a program changed the value of \(\pi\) in the middle of its execution. As programmers, we can make a promise to never change those variables that should not change. But the language allows a way to enforce the idea.
A const
is a named constant.
It can’t be assigned a new value after initialization.
For example:
const double pi = 3.14159265359;
double area = pi * r * r; // OK to read pi like any other variable
pi = 3; // this is a compile error
Returning to our by_reference function from the previous section, we have seen that pass by reference makes passing data to functions cheap:
void by_reference (int& x) {
std::cout << "in by_ref the address of x is " << &x << '\n';
x = -1;
}
But one of the side-effects of making passing cheap,
is that now the called function might change the value provided.
As programmers, we could promise to not change values in functions that shouldn’t,
but generally, we don’t like promises.
Programmers prefer contracts.
Fortunately, const
allows us to define just such a contract in the declaration:
void by_reference (const int& x) {
std::cout << "in by_ref the address of x is " << &x << '\n';
x = -1; // this is now a compile error
}
This is called passing by constant reference, or “const ref”, for short.
Passing by const ref allows us to enforce our intentions.
If a variable is declared as const
, it is a compile error to change it.
Any callers can use this function and rest assured that their data cannot change.
The more time you spend programming,
the more you will appreciate how powerful that guarantee is.
C99 added the const
keyword, so now it’s in both languages,
but you don’t see it nearly as often in C.
Many programmers use #define
instead.
3.6.1. Prefer const
to #define
¶
We also prefer inline specifier and enum declaration over define.
There are good reasons to avoid #define
where alternatives exist.
#define
is parsed by the preprocessor, not the compiler.
This mean that effectively, all #define
directives are literally strings.
Fundamentally they are no different from any other pre-processor directive (#include
, #ifdef
, etc.),
except that people commonly use #define
as a placeholder for a numeric type,
or a function.
For example:
#define ASPECT_RATIO 1.653
is an old fashioned way to define a constant, but you’ll likely see it ‘in the wild’. The pre-processor literally copies the value ‘1.653’ every place in the source code it encounters the string ‘ASPECT_RATIO’. Then the program is compiled.
Prefer this instead:
const double ASPECT_RATIO = 1.653;
This version preserves the name ‘ASPECT_RATIO’ which can simplify debugging.
It is possible to also preserve macro names using
certain debugging compiler switches, such as -g3
.
3.6.1.1. Function-like macros using #define
¶
If you use #define
to create a function-like macro, then unexpected behaviors are possible.
For example,
a macro to call some function f()
with the larger of either a
or b
:
#include <iostream>
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
int f(const int x) {
return x;
}
int main() {
int a = -5;
const int b = 0;
for (int i = 1; i < 11; ++i) {
CALL_WITH_MAX(++a, b); // call f, but throw away the result
std::cout << i << ", a: " << a << '\n'; // a is one larger each iteration
}
a = -5;
for (int i = 1; i < 11; ++i) {
CALL_WITH_MAX(++a, b+10);
std::cout << i << ", a: " << a << '\n';
}
}
The number of times a
is incremented depends on how CALL_WITH_MAX
is called.
In this case, the value of b
affects whether a
is incremented once or twice.
Ouch!
Step through example 17 and see for yourself.
#define
may seem like a shortcut.
It’s not.
Use it when no better alternative exists.
3.6.2. Keyword: constexpr
¶
The keyword constexpr was added in C++11.
The fundamental difference between const
and constexpr
is that
constexpr
must be able to be determined at compile time.
So while this is OK:
constexpr double pi = 3.14159265359;
// and so is this
constexpr double pi = acos(-1);
// and so is this
constexpr double area (const double radius) {
return pi * radius * radius;
}
This is not OK in C++11:
constexpr double area (const double radius) {
assert (radius > 0);
return pi * radius * radius;
}
Adding a simple assertion causes this function to no longer compile:
g++ -std=c++11 -Wall -Wextra -pedantic area.cpp -o area
area.cpp: In function ‘constexpr double area(double)’:
area.cpp:8:1: error: body of constexpr function ‘constexpr double area(double)’ not a return-statement
}
^
On compilers that support C++14, if you compile with -std=c++11
you may see a warning like:
warning: use of this statement in a constexpr function is a C++14 extension [-Wc++14-extensions]
assert (radius > 0);
There are some exceptions, but in C++11,
any function more complex than return (some_expression)
is not able to be evaluated at compile time,
therefore, it won’t compile as a constexpr
expression.
You should still use it when you can.
The rules for constexpr
changed in C++14 and more in C++17.
Although not available on the Mesa server, you should still
read the docs
and be aware of what is and is not a valid constant expression
for whatever environment you are working in.
3.6.3. Guidelines for now¶
When creating local variables
Ask: “Does this variable ever change?”
If not, consider
const
orconstexpr
Recall
constexpr
is more restrictiveConstant expression is evaluated at compile time
When passing parameters to functions
Consider passing by
const
referenceApplies only to object types
Pass fundamental types by
const
value if they should not change
Avoids making an extra (unneeded) copy
Prevents unintended modification
Try This!
Given the following:
How many simple changes can you make to the function area
that are valid
if the function signature is
const double area (const double radius)
but invalid if the function signature is unchanged?
More to Explore
From: cppreference.com: const qualifier and constexpr
C++ Core Guidelines for constexpr from GitHub