A constant in programming refers to a value or identifier that remains fixed and unchanging throughout the execution of a program. In C++, constants are categorized into two main types:
- Named Constants: Also known as symbolic constants, these constants are associated with a specific identifier. They provide a meaningful name to a constant value, making the code more readable and easier to maintain.
- Literal Constants: These are constant values directly used in the code without any associated identifier. They represent fixed values like numbers, characters, or strings, and are used directly within expressions.
- Literal constants represent fixed values directly in the code, such as numbers, characters, and strings. These constants have no identifier but are used directly in expressions.
int age = 30; // '30' is a literal constant char grade = 'A'; // 'A' is a literal constant
Types of named constants
There are three ways to define a named constant in C++:
const
Keyword- Using
#define
Preprocessor - Constant Member Variable in Classes
- Enumerated constants
constexpr
Keyword Constantsconsteval
Keyword (C++20)
1️⃣ const
Keyword
A variable whose value cannot be changed is called a constant variable.
The keyword const
is used to define constant variables whose values cannot be modified after initialization. Once a variable is declared as const
, it becomes read-only throughout its lifetime. However it doesn't guarantee that the value is known at compile-time.
-: Declaring a const
variable :-
Constants can be declared using the const
keyword, also known as a const qualifier
. However, there's a preferred and a less-preferred way to position this qualifier in your code.
Syntax:
const type name = value;
// OR
type const name = value;
Examples:
const double PI { 3.14 }; // Preferred use of const before the type
int const four { 4 }; // Acceptable, but not the preferred style
While C++ allows the const
qualifier to be placed either before or after the type, adhering to the convention of placing it before the type is recommended. This aligns with standard English language conventions, where modifiers typically precede the object they modify, as seen in phrases like “a green ball”, not “a ball green.”
Place
const
before the type (because it is more conventional to do so)
const
variables must be initialized:
Const
variables must be initialized when you define them, and then that value can not be changed via assignment:
int main()
{
const double pi; // error: const variables must be initialized
pi = 3.14; // error: const variables can not be changed
return 0;
}
- Note that
const
variables can be initialized from other variables (includingnon-const
ones):
#include <iostream>
int main()
{
std::cout << "Enter your age: ";
int age{};
std::cin >> age;
const int constAge { age }; // initialize const variable using non-const value
age = 5; // ok: age is non-const, so we can change its value
constAge = 6; // error: constAge is const, so we cannot change its value
return 0;
}
// Test the above code at cpp.sh
// with c++11.
and comment the line, constAge = 6.
We initialize const variable constAge
with non-const variable age
. Because age
is still non-const, we can change its value. However, because constAge
is const change the value it has after initialization.
Key Points About const
:
- Compile-time or runtime: The value of a
const
variable can either be known at compile-time or determined at runtime.- For example, it it's initialized with a constant expression (like
100
), it is evaluated at compile-time. But if initialized with a runtime value (e.g., from user input), it is runtime constant.const int x = 10; // Known at compile-time int y; std::cin >> y; const int z = y; // Runtime constant, value determined at runtime
- It's alternative is
constexpr
keyword inC++
.
- For example, it it's initialized with a constant expression (like
- Immutable: Once initialized, a
const
variable's value cannot be changed. - Use with pointers and references: In the case of pointers, the
const
qualifier can be applied to either the pointer itself or the data being pointed to (or both) or with references.const int* ptr; // ptr is a pointer to a constant integer. You can modify the pointer but not the value it points to. int* const ptr; // ptr is a constant pointer to an integer. You can modify the value, but not the pointer itself. const int* const ptr; // ptr is a constant pointer to a constant integer. You cannot modify either.
- Const correctness: When passing arguments to functions,
const
can enforce that the function won’t modify its inputs:void processData(const int& data) { // data cannot be modified within this function }
- Global or Local:
const
can be used to declare global constants or local variables inside functions. - Type Safety: The
const
keyword ensures type safety, which means that once declared, the compiler will prevent any modification to the constant variable.
Const
function parameters :-
Function parameters can be made constants via the const
keyword:
#include <iostream>
void printInt(const int x)
{
std::cout << x << '\n';
}
int main()
{
printInt(5); // 5 will be used as the initializer for x
printInt(6); // 6 will be used as the initializer for x
return 0;
}
- Note that we did not provide an explicit initializer for our
const
parameterx
– the value of the argument in the function call will be used as the initializer forx
.
Const
return values :-
A function's return value may also be made const
:
#include <iostream>
const int getValue()
{
return 5;
}
int main()
{
std::cout << getValue() << '\n';
return 0;
}
// Output
5
-: const
Pointers :-
There are two types of const
when it comes to pointers. const
pointers are pointers whose behavior is influenced by the const
keyword, either by making the pointer itself constant or the data being pointed to constant.
1 Pointer to a Constant (const T* ptr
):
A pointer to a constant means that the data being pointed to is constant, but the pointer itself is not. This means you cannot modify the value that the pointer is pointing to, but you can change the pointer to point to a different memory location.
Syntax:
const int* ptr;
- Characteristics:
- You can change what the pointer points to, but you cannot modify the value it points to through the pointer.
- The value being pointed to is read-only via this pointer.
- The pointer itself is modifiable.
- Practical Uses Cases:
- Used when you want to protect the data being pointed to from modification but still allow the pointer to change its target.
- Example: In function parameters to ensure the function does not modify the data.
void printValue(const int* ptr) { // ptr points to constant data, so you cannot modify *ptr std::cout << *ptr << std::endl; }
Example:
int a = 10;
int b = 20;
const int* ptr = &a; // Pointer to constant integer (cannot modify 'a' through ptr)
*ptr = 15; // Error: cannot modify the value of a constant
ptr = &b; // OK: you can change what the pointer points to
In this case, ptr
can point to different integers (a
or b
), but it cannot be used to change the values of a
or b
.
2 Constant Pointer (T* const ptr
):
A constant pointer means that the pointer itself is constant, but the data being pointed to is not. This means you cannot change the memory address the pointer is pointing to, but you can modify the value at that address.
Syntax:
int* const ptr;
- Characteristics:
- You cannot change what the pointer points to after it has been initialized.
- You can modify the value of the data the pointer points to.
- Use Cases:
- Used when you want to fix the pointer to a specific object but allow the modification of the object itself.
- Example: To ensure that the pointer always points to a specific object, like a hardware register in embedded systems.
int value = 42; int* const p = &value; // Pointer must always point to 'value' *p = 100; // OK
Example:
int a = 10;
int* const ptr = &a; // Constant pointer to an integer
*ptr = 15; // OK: you can modify the value of 'a'
ptr = &b; // Error: cannot change the address the pointer points to
In this example, ptr
must always point to a
after initialization, but the value of a
can be modified.
3 Constant Pointer to Constant Data (const T* const ptr
):
A constant pointer to constant data means that both the pointer and the data being pointed to are constant. This means you cannot change where the pointer points to, and you cannot modify the value of the data it points to.
Syntax:
const int* const ptr;
- Characteristics:
- You cannot change the address the pointer holds (the pointer is constant).
- You cannot modify the value of the data the pointer points to (the data is constant).
- Use Cases:
- Used when both the pointer and the data it points to should be immutable.
- Example: When a function or object needs to use a fixed, read-only value and ensure it remains constant.
int value = 10; const int* const p = &value;
Example:
int a = 10;
const int* const ptr = &a; // Constant pointer to constant integer
*ptr = 15; // Error: cannot modify the value of a constant
ptr = &b; // Error: cannot change the address of a constant pointer
Summarizing the Types of Const Pointers
Type | Meaning | Example Syntax |
---|---|---|
Pointer to Constant | The value pointed to is constant, but the pointer itself can change to point to other data. | const int* ptr; |
Constant Pointer | The pointer itself is constant (it cannot point to a different memory address), but the data it points to can change. | int* const ptr; |
Constant Pointer to Constant Data | Both the pointer and the value it points to are constant, so neither can be changed. | const int* const ptr; |
2️⃣ #define
Preprocessor Constants:
The #define
directive is a preprocessor macro used to define constants. These constants are handled by the preprocessor, not the compiler, so they don’t have a type, and their scope is not restricted like variables. #define
is essentially a text substitution mechanism.
Example:
#define PI 3.14159
#define MAX_BUFFER_SIZE 1024
While commonly used in C, #define
is discouraged in modern C++ in favor of const
or constexpr
due to the lack of type safety and the potential for difficult debugging.
Disadvantages of #define
:
- No type safety: Since
#define
simply performs text substitution, it does not perform type checking. - Harder to debug: Errors involving
#define
constants can be difficult to trace, as the preprocessor simply replaces occurrences of the constant name with the value.
3️⃣ Constant Member Variable in Classes:
In object-oriented C++, member variables of a class can be declared as constants using the const
keyword. These constant member variables must be initialized in the constructor initializer list or directly at the time of declaration.
Example:
class Circle {
public:
const float PI = 3.14159;
float radius;
Circle(float r) : radius(r) {}
};
Constant member variables cannot be modified once initialized, providing additional safety within class objects.
4️⃣ Enumerations (enum
)
An enum
is a user-defined data type consisting of named integer constants. Enumerations provide a way to assign symbolic names to integral values, making the code more readable.
Syntax:
enum EnumName { enumerator1, enumerator2, ..., enumeratorN };
Example:
enum Day { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday };
By default, the first enumerator has a value of 0
, and each subsequent enumerator increments by 1
. You can explicitly assign integer values to enumerators as well.
Example with Explicit Values:
enum ErrorCode { Success = 0, FileNotFound = 404, PermissionDenied = 403 };
Benefits of Enumerations:
- Improved readability by using meaningful names instead of raw integers.
- Type safety: Enumerations create a new type, which reduces accidental mixing of integer constants with enumeration constants.
5️⃣ constexpr
Keyword Constants
constexpr
is used to declare compile-time constants, ensuring that the constant expression is evaluated during compilation. This allows for optimizations and ensures that the value is immutable. It is more restrictive than const
.
Syntax:
constexpr type variable = value;
Example:
constexpr int MAX_SIZE = 100; // Must be evaluated at compile-time
Characteristics:
- Compile-time Evaluation: A
constexpr
variable or function guarantees that the value will be evaluated at compile-time. This means the value must be known at the time of compilation, and the expression used must be a constant expression. - Functions as
constexpr
: One of the most powerful features ofconstexpr
is that it can be applied to functions. Aconstexpr
function is evaluated at compile-time if its arguments are constant expressions. If the arguments are not known at compile-time, the function will behave like a normal function and be evaluated at runtime. - Strict Requirements: A
constexpr
variable must be initialized with a constant expression (a value that the compiler can determine at compile-time). You cannot assign runtime values to aconstexpr
variable. - Introduced in C++11, Enhanced in Later Versions:
constexpr
was introduced in C++11 and has been enhanced in C++14 and C++20, with more flexible usage and the ability to declare more complex functions asconstexpr
.
Example of a constexpr
Function:
constexpr int square(int x) {
return x * x;
}
constexpr int result = square(5); // Evaluated at compile-time
int main() {
int runtime_value;
std::cin >> runtime_value;
int runtime_result = square(runtime_value); // Evaluated at runtime
}
6️⃣ consteval
Keyword (C++20)
Introduced in C++20, consteval
ensures that a function is evaluated at compile-time. This is stricter than constexpr
, as consteval
requires the function to be called with compile-time arguments.
Example:
consteval int getConstant() {
return 42;
}
int main() {
constexpr int val = getConstant(); // Must be evaluated at compile-time
}
Characteristics:
- Ensures the function is evaluated at compile-time.
- Useful for defining values that must be determined during compilation.
For example:
#include <iostream>
#define MY_NAME "MaK"
int main()
{
std::cout << "My name is: " << MY_NAME << '\n';
return 0;
}
// Output
My name is: MaK
When the preprocessor processes the file containing this code, it will replace MY_NAME
with "MaK"
. Note that MY_NAME
is a name, and the substitution text is a constant value, so object-like macros with substitution text are also named constants.
Prefer constant variables to preprocessor macros
1. The biggest issue is that macros don't follow normal C++ scoping rules.
Once a macro is #defined
, all subsequent occurrences of the macro's name in the current file will be replaced. If that name is used elsewhere, you will get macro substitution where you didn't want it. This will most likely lead to strange compilation errors.
For example:
#include <iostream>
void someFcn()
{
// Even though gravity is defined inside this function
// the preprocessor will replace all subsequent occurrences of gravity in the rest of the file
#define gravity 9.8
}
void printGravity(double gravity) // including this one, causing a compilation error
{
std::cout << "gravity: " << gravity << '\n';
}
int main()
{
printGravity(3.71);
return 0;
}
When compiled, GCC produced this confusing error:
error: expected ',' or '...' before numeric constant
7 | #define gravity 9.8
| ^~~
<source>:10:26: note: in expansion of macro 'gravity'
10 | void printGravity(double gravity) // including this one, causing a compilation error
2. It is often harder to debug code using macros.
Although your source code will have the macro's name, the compiler and debugger never see the macro because it has already been replaced before they run. Many debuggers are unable to inspect a macro's value, and often have limited capabilities when working with macros.
Because of these problems, prefer constant variables over object-like macros with substitution text.
Type qualifiers
Type qualifiers in C++ are essential components that modify the behavior of types, providing additional information to the compiler about how variables of those types should be treated. As of now C++21 these type qualifiers exist:
1. const
Type Qualifier:
The const
type qualifier, short for “constant”, is used to declare variables indicate that a particular type should not be modified. Once a variable is declared as const
, its value cannot be changed during its lifetime.
Example:
const int MAX_VALUE = 100;
const double PI = 3.14159;
const int* ptr = &MAX_VALUE; // Constant pointer to an integer
const double& ref = PI; // Constant reference to a double
Key Points:
- Applied to variables, pointers, or references.
- Ensures that the value of the variable cannot be modified.
- Commonly used for defining constants, function parameters that should not be modified, and preventing unintentional modifications in functions.
2. volatile
Type Qualifier:
The volatile
type qualifier is used to indicate that a variable's value can be changed at any time without any action being taken by the code that references it. It is often used when dealing with hardware-related or memory-mapped variables that can be modified externally.
Example:
volatile int sensorValue = 0;
volatile int* sensorPtr = &sensorValue; // Pointer to a volatile integer
Key Points:
- Informs the compiler that the value of the variable may change asynchronously.
- Prevents the compiler from optimizing away reads or writes to the variable.
- Frequently used in scenarios where variables can be modified by external processes, such as hardware.
3. mutable
Type Qualifier:
The mutable
type qualifier is applied to a class member variable, indicating that the variable can be modified even within a const
member function. This allows for selective modification of certain class members while maintaining the logical constness of the object.
Example:
class Example {
public:
mutable int counter;
void updateCounter() const {
// Allowed modification of a mutable member in a const function
counter++;
}
};
Key Points:
- Applied to a member variable within a class.
- Permits modification of the member variable even withing
const
member functions. - Useful in situations where a member variable needs to be updated for bookkeeping purposes without altering the external behavior of the class.
Use Case Scenario:
Consider a scenario where a class maintains a counter to track the number of times a certain operation is performed. The counter needs to be updated each time the operation occurs, even within functions marked as const
. The mutable
qualifier enables this behavior.
class OperationCounter {
public:
mutable int counter;
OperationCounter() : counter(0) {}
void performOperation() const {
// Modify the mutable member even in a const member function
counter++;
}
int getCount() const {
return counter;
}
};
OperationCounter obj;
obj.performOperation();
std::cout << "Operation Count: " << obj.getCount() << std::endl;
// Outputs: 1