Both const
and constexpr
are keywords used to specify that a value cannot be modified, but they have some differences in functionality and use cases.
What is const
?
const
is a fundamental keyword in C++ used for defining constant variables, meaning the value of the variable cannot change. Look at the following simple example of this concept:
int main() {
const int a = 0;
a = 5; // error
return 0;
}
In addition to constant variables, const
can be used for member functions. Look at the following class:
class MyClass {
public:
void SetValue(int newValue);
int GetValue() const;
private:
int m_value;
}
void MyClass::SetValue(int newValue) {
m_value = newValue;
}
int MyClass::GetValue() const {
return m_value;
}
In this example, GetValue()
is defined as const
, which promises that m_value
will remain unmodified within the GetValue()
function. In contrast, SetValue(int newValue)
is not const
, so m_value
is allowed to be modified.
Consider the following modification:
void MyClass::SetValue(int newValue) const {
m_value = newValue; // error
}
Here, SetValue(int newValue)
is defined as const
, which results in an error because an object’s state cannot be modified within its own const
member functions.
Another important feature of const
is that both const
and non-const
member functions can be called by non-const
variables, but const
variables can only call const
functions. For example:
int main() {
MyClass obj1;
const MyClass obj2;
obj1.SetValue(10); // No error
int obj1Value = obj1.GetValue(); // No error
obj2.SetValue(10); // Error
int obj2Value = obj2.GetValue(); // No error
return 0;
}
In this example, we observe that the const MyClass obj2
variable is not allowed to call the non-const
member function. This makes sense, because if we want obj2
to be const
, then we intend for it to remain constant, so changing its state with a setter function would be invalid.
Personally, here is how I would define MyClass
in most cases:
class MyClass {
public:
void SetValue(int newValue);
const int& GetValue() const;
private:
int m_value;
}
In most cases, a getter function is used to get a read-only version of an object’s state. Although some cases may require it, you generally should avoid using a getter function to get a value that you plan to modify. Therefore, making GetValue()
a const
function, as well as returning a const
reference is the most efficient and best practice (even though returning a reference to an int
is kind of unnecessary but good practice for returning objects).
What is constexpr
?
Beginning in C++11, constexpr
was added to the C++ standard and later improved in C++14. It is used to declare constant expressions, hence the name constexpr
. These expressions are very similar in functionality to const
; in fact, all constexpr
variables are const
by default. However, there are a few key differences.
The most important difference is that constexpr
values are initialized at compile-time rather than runtime. This means that a constexpr
value can must be known at compile-time or determined by another constexpr
value. If you are trying to hyper-optimize your code, opt for compile-time constexpr
values whenever possible.
Furthermore, constexpr
can be applied to functions. This is useful for instances where a calculation needs to be performed but will always remain the same, such as the following implementation of a Fahrenheit to Celsius conversion function:
constexpr double FahrenheitToCelsius(double fahrenheit) {
return (fahrenheit - 32.0) * 5.0 / 9.0;
}
constexpr double c_waterBoilingPointFahrenheit = 212.0;
constexpr double c_waterBoilingPointCelsius = FahrenheitToCelsius(c_waterBoilingPointFahrenheit);
This calculation is determined at compile-time rather than runtime like it would be if FahrenheitToCelsius
was defined as a standard function.
Another great example of constexpr
is to use a constexpr
array for a lookup table. For example:
constexpr int Factorial(int n) {
return (n <= 1) ? 1 : n * Factorial(n - 1);
}
int main() {
constexpr int factorialTable[10] = {
Factorial(0), Factorial(1), Factorial(2), Factorial(3), Factorial(4),
Factorial(5), Factorial(6), Factorial(7), Factorial(8), Factorial(9)
};
constexpr int factorialOfFive = factorialTable[5];
return 0;
}
With a lookup table, we only need to calculate values once at compile-time, which improves performance in cases where a value may normally need to be calculated multiple times.
Conclusion
Both const
and constexpr
are important features of modern C++ development that should be used extensively.
Any time you expect a value to remain constant, like when retrieving the value of a member variable of an object using a getter function, you should use const
— for both the variable used to store the value and in the getter function itself.
If you need to define a constant value, such as a buffer size or the number of degrees in a circle, it is best practice to use constexpr
, ideally with the c_
prefix, similar to using m_
for member variables. If your constant needs to be calculated but will always have the same result, then the calculation function should also be constexpr
to take advantage of the compile-time speed.