Understanding Constants

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:

  1. 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.
  2. 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++:

  1. const Keyword
  2. Using #define Preprocessor
  3. Constant Member Variable in Classes
  4. Enumerated constants
  5. constexpr Keyword Constants
  6. consteval 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 (including non-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 in C++.
  • 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 parameter x – the value of the argument in the function call will be used as the initializer for x.

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
TypeMeaningExample Syntax
Pointer to ConstantThe value pointed to is constant, but the pointer itself can change to point to other data.const int* ptr;
Constant PointerThe 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 DataBoth 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 of constexpr is that it can be applied to functions. A constexpr 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 a constexpr 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 as constexpr.

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