Skip to content

C++ Constants

Overview

Constants are data whose values cannot be modified during program execution. C++ provides multiple methods for defining constants, including the const keyword, constexpr keyword, macro definitions, and more. Using constants correctly improves code safety, readability, and performance.

🔒 const Keyword

Basic const Variables

cpp
#include <iostream>

int main() {
    // Basic const variables
    const int MAX_SIZE = 100;
    const double PI = 3.14159;
    const char GRADE = 'A';
    const std::string MESSAGE = "Hello, World!";
    
    std::cout << "Max size: " << MAX_SIZE << std::endl;
    std::cout << "Pi: " << PI << std::endl;
    std::cout << "Grade: " << GRADE << std::endl;
    std::cout << "Message: " << MESSAGE << std::endl;
    
    // The following code will cause compilation errors
    // MAX_SIZE = 200;  // Error! Cannot modify const variable
    // PI = 3.14;       // Error! Cannot modify const variable
    
    // const variables must be initialized at declaration
    // const int UNINITIALIZED;  // Error! const variable must be initialized
    
    return 0;
}

const Pointers

cpp
#include <iostream>

int main() {
    int value1 = 10;
    int value2 = 20;
    
    // 1. Pointer to const object (the pointed-to value cannot be modified)
    const int* ptr_to_const = &value1;
    std::cout << "Pointer to const: " << *ptr_to_const << std::endl;
    
    // *ptr_to_const = 30;  // Error! Cannot modify value through pointer
    ptr_to_const = &value2; // Correct! Can change the pointer itself
    std::cout << "After changing pointer: " << *ptr_to_const << std::endl;
    
    // 2. const pointer (the pointer itself cannot be modified)
    int* const const_ptr = &value1;
    std::cout << "const pointer: " << *const_ptr << std::endl;
    
    *const_ptr = 30;        // Correct! Can modify the pointed-to value
    // const_ptr = &value2; // Error! Cannot modify the pointer itself
    std::cout << "After modifying value: " << *const_ptr << std::endl;
    
    // 3. const pointer to const object
    const int* const const_ptr_to_const = &value2;
    std::cout << "const pointer to const: " << *const_ptr_to_const << std::endl;
    
    // *const_ptr_to_const = 40;    // Error! Cannot modify value
    // const_ptr_to_const = &value1; // Error! Cannot modify pointer
    
    return 0;
}

const References

cpp
#include <iostream>
#include <string>

// Using const reference as function parameter (avoid copying, prevent modification)
void print_string(const std::string& str) {
    std::cout << "String: " << str << std::endl;
    // str += " modified";  // Error! Cannot modify const reference
}

// Return const reference
class Person {
private:
    std::string name_;
    int age_;
    
public:
    Person(const std::string& name, int age) : name_(name), age_(age) {}
    
    // Return const reference, prevent external modification
    const std::string& getName() const {
        return name_;
    }
    
    // Non-const version, allows modification
    std::string& getName() {
        return name_;
    }
    
    int getAge() const { return age_; }
};

int main() {
    std::string message = "Hello, C++!";
    
    // const reference can bind to lvalues
    const std::string& ref1 = message;
    std::cout << "const reference: " << ref1 << std::endl;
    
    // const reference can bind to rvalues
    const std::string& ref2 = "Temporary String";
    std::cout << "Bind to rvalue: " << ref2 << std::endl;
    
    // Using const reference parameters
    print_string(message);
    print_string("Direct String");
    
    // const references in class
    Person person("Alice", 25);
    const Person const_person("Bob", 30);
    
    std::cout << "Regular object name: " << person.getName() << std::endl;
    std::cout << "const object name: " << const_person.getName() << std::endl;
    
    // Modify non-const object
    person.getName() = "Alice Smith";
    std::cout << "After modification name: " << person.getName() << std::endl;
    
    return 0;
}

⚡ constexpr Keyword (C++11)

Compile-Time Constants

cpp
#include <iostream>
#include <array>

// constexpr function
constexpr int square(int x) {
    return x * x;
}

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

// constexpr class
class Rectangle {
private:
    int width_, height_;
    
public:
    constexpr Rectangle(int w, int h) : width_(w), height_(h) {}
    
    constexpr int area() const {
        return width_ * height_;
    }
    
    constexpr int perimeter() const {
        return 2 * (width_ + height_);
    }
};

int main() {
    // constexpr variables are computed at compile time
    constexpr int SIZE = 10;
    constexpr int SQUARED = square(5);      // Computed at compile time
    constexpr int FACT_5 = factorial(5);   // Computed at compile time
    
    std::cout << "SIZE: " << SIZE << std::endl;
    std::cout << "Square of 5: " << SQUARED << std::endl;
    std::cout << "Factorial of 5: " << FACT_5 << std::endl;
    
    // Can be used for array size (must be compile-time constant)
    constexpr int ARRAY_SIZE = square(4);
    std::array<int, ARRAY_SIZE> arr;
    std::cout << "Array size: " << arr.size() << std::endl;
    
    // constexpr object
    constexpr Rectangle rect(3, 4);
    constexpr int area = rect.area();       // Computed at compile time
    constexpr int perimeter = rect.perimeter(); // Computed at compile time
    
    std::cout << "Rectangle area: " << area << std::endl;
    std::cout << "Rectangle perimeter: " << perimeter << std::endl;
    
    // Runtime values cannot be used in constexpr
    int runtime_value;
    std::cin >> runtime_value;
    // constexpr int bad = square(runtime_value); // Error!
    int good = square(runtime_value);              // Correct, runtime computation
    
    return 0;
}

constexpr vs const

cpp
#include <iostream>

int get_runtime_value() {
    return 42;
}

int main() {
    // const: runtime constant, value cannot be modified after being determined at runtime
    const int runtime_const = get_runtime_value();
    std::cout << "Runtime constant: " << runtime_const << std::endl;
    
    // constexpr: compile-time constant, value must be determined at compile time
    constexpr int compile_time_const = 42;
    std::cout << "Compile-time constant: " << compile_time_const << std::endl;
    
    // Error examples
    // constexpr int bad = get_runtime_value(); // Error! Not a compile-time constant
    
    // constexpr can be used for template parameters and array sizes
    constexpr int TEMPLATE_PARAM = 10;
    std::array<int, TEMPLATE_PARAM> compile_time_array;
    
    // const cannot be used where compile-time constant is needed
    // std::array<int, runtime_const> runtime_array; // Error!
    
    // constexpr variables are also const
    // compile_time_const = 50; // Error! constexpr variable cannot be modified
    
    return 0;
}

📍 Macro Definitions (#define)

Basic Macro Definitions

cpp
#include <iostream>

// Simple macro definitions
#define MAX_SIZE 1000
#define PI 3.14159
#define MESSAGE "Hello from macro"

// Function-like macros
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))

// Multi-line macros
#define PRINT_INFO(name, age) \
    do { \
        std::cout << "Name: " << (name) << std::endl; \
        std::cout << "Age: " << (age) << std::endl; \
    } while(0)

// Conditional compilation macros
#define DEBUG_MODE 1

#if DEBUG_MODE
    #define DEBUG_PRINT(x) std::cout << "DEBUG: " << (x) << std::endl
#else
    #define DEBUG_PRINT(x)
#endif

int main() {
    std::cout << "Max size: " << MAX_SIZE << std::endl;
    std::cout << "Pi: " << PI << std::endl;
    std::cout << "Message: " << MESSAGE << std::endl;
    
    // Using function-like macros
    int a = 5, b = 3;
    std::cout << "Square of 5: " << SQUARE(a) << std::endl;
    std::cout << "Max value: " << MAX(a, b) << std::endl;
    std::cout << "Absolute value: " << ABS(-10) << std::endl;
    
    // Using multi-line macros
    PRINT_INFO("John", 25);
    
    // Debug macros
    DEBUG_PRINT("This is debug information");
    
    return 0;
}

Macro Problems and Considerations

cpp
#include <iostream>

#define BAD_SQUARE(x) x * x          // Dangerous macro definition
#define GOOD_SQUARE(x) ((x) * (x))   // Safe macro definition

#define INCREMENT(x) ++x             // Side effect problem

int main() {
    // Problem 1: Operator precedence issue
    int result1 = BAD_SQUARE(2 + 3);   // Expects 25, actually 11 (2 + 3 * 2 + 3)
    int result2 = GOOD_SQUARE(2 + 3);  // Correct result 25 ((2 + 3) * (2 + 3))
    
    std::cout << "BAD_SQUARE(2 + 3): " << result1 << std::endl;
    std::cout << "GOOD_SQUARE(2 + 3): " << result2 << std::endl;
    
    // Problem 2: Side effect issue
    int x = 5;
    int result3 = GOOD_SQUARE(INCREMENT(x)); // x is incremented twice!
    std::cout << "Value of x: " << x << std::endl; // x = 7, not the expected 6
    
    // Problem 3: Type unsafe
    double d = 2.5;
    std::cout << "GOOD_SQUARE(2.5): " << GOOD_SQUARE(d) << std::endl;
    
    // Better solution: use constexpr functions
    constexpr auto safe_square = [](auto x) { return x * x; };
    std::cout << "lambda square: " << safe_square(2 + 3) << std::endl;
    
    return 0;
}

🔢 Enumerated Constants

Traditional Enums

cpp
#include <iostream>

// Traditional enum
enum Color {
    RED,        // 0
    GREEN,      // 1
    BLUE        // 2
};

enum Status {
    PENDING = 1,
    RUNNING = 5,
    STOPPED = 10
};

// Anonymous enum used as constant
enum {
    BUFFER_SIZE = 1024,
    MAX_CONNECTIONS = 100
};

int main() {
    Color background = RED;
    Status current_status = RUNNING;
    
    std::cout << "Background color: " << background << std::endl;
    std::cout << "Current status: " << current_status << std::endl;
    
    // Enums can be implicitly converted to integers
    int color_value = background;
    std::cout << "Color value: " << color_value << std::endl;
    
    // Using enum constants
    char buffer[BUFFER_SIZE];
    std::cout << "Buffer size: " << sizeof(buffer) << std::endl;
    
    return 0;
}

Strongly-Typed Enums (C++11)

cpp
#include <iostream>

// Strongly-typed enum (enum class)
enum class Direction {
    North,
    South,
    East,
    West
};

enum class ErrorCode : int {
    Success = 0,
    FileNotFound = 1001,
    AccessDenied = 1002,
    NetworkError = 2001
};

// Specifying underlying type
enum class Priority : char {
    Low = 'L',
    Medium = 'M',
    High = 'H'
};

int main() {
    Direction player_direction = Direction::North;
    ErrorCode last_error = ErrorCode::Success;
    Priority task_priority = Priority::High;
    
    // Strongly-typed enums cannot be implicitly converted to integers
    // int dir_value = player_direction;  // Error!
    int dir_value = static_cast<int>(player_direction); // Requires explicit cast
    
    std::cout << "Direction value: " << dir_value << std::endl;
    std::cout << "Error code: " << static_cast<int>(last_error) << std::endl;
    std::cout << "Priority: " << static_cast<char>(task_priority) << std::endl;
    
    // Strongly-typed enums avoid name conflicts
    enum class Color { Red, Green, Blue };
    enum class Traffic { Red, Yellow, Green };
    
    Color car_color = Color::Red;
    Traffic light_state = Traffic::Red;
    // No conflict, type-safe
    
    return 0;
}

🏛️ Static Constant Members

Constants in Classes

cpp
#include <iostream>
#include <string>

class MathConstants {
public:
    // static const member (integral types can be initialized in-class)
    static const int MAX_ITERATIONS = 1000;
    static const char DELIMITER = ',';
    
    // Non-integral static const members need to be defined outside the class
    static const double PI;
    static const std::string DEFAULT_NAME;
    
    // C++11: static constexpr members
    static constexpr double E = 2.71828;
    static constexpr int ARRAY_SIZE = 10;
};

// Define static const members outside the class
const double MathConstants::PI = 3.14159265359;
const std::string MathConstants::DEFAULT_NAME = "Unknown";

class Configuration {
private:
    const int id_;              // Instance const member
    const std::string name_;    // Instance const member
    
public:
    // const members must be initialized in the initializer list
    Configuration(int id, const std::string& name) 
        : id_(id), name_(name) {}
    
    // const member functions
    int getId() const { return id_; }
    const std::string& getName() const { return name_; }
    
    // const members cannot be modified
    // void setId(int new_id) { id_ = new_id; }  // Error!
};

int main() {
    // Access static constant members
    std::cout << "Max iterations: " << MathConstants::MAX_ITERATIONS << std::endl;
    std::cout << "Pi: " << MathConstants::PI << std::endl;
    std::cout << "Euler's number: " << MathConstants::E << std::endl;
    std::cout << "Default name: " << MathConstants::DEFAULT_NAME << std::endl;
    
    // Using constant array size
    int array[MathConstants::ARRAY_SIZE];
    std::cout << "Array size: " << sizeof(array) / sizeof(int) << std::endl;
    
    // Instance const members
    Configuration config(123, "MyConfig");
    std::cout << "Config ID: " << config.getId() << std::endl;
    std::cout << "Config name: " << config.getName() << std::endl;
    
    return 0;
}

🛡️ const Correctness

const Member Functions

cpp
#include <iostream>
#include <vector>

class Container {
private:
    std::vector<int> data_;
    mutable int access_count_;  // mutable allows modification in const functions
    
public:
    Container() : access_count_(0) {}
    
    // Non-const member function
    void add(int value) {
        data_.push_back(value);
    }
    
    // const member function (promises not to modify object state)
    size_t size() const {
        access_count_++;        // mutable member can be modified
        return data_.size();
    }
    
    // const version accessor
    const int& at(size_t index) const {
        if (index >= data_.size()) {
            throw std::out_of_range("Index out of range");
        }
        access_count_++;
        return data_[index];
    }
    
    // Non-const version accessor
    int& at(size_t index) {
        if (index >= data_.size()) {
            throw std::out_of_range("Index out of range");
        }
        access_count_++;
        return data_[index];
    }
    
    // const member functions can call other const member functions
    void print() const {
        std::cout << "Container size: " << size() << std::endl;
        for (size_t i = 0; i < size(); ++i) {
            std::cout << at(i) << " ";
        }
        std::cout << std::endl;
        std::cout << "Access count: " << access_count_ << std::endl;
    }
};

// Function overloading: const and non-const versions
void process_container(Container& container) {
    std::cout << "Processing non-const container" << std::endl;
    container.add(42);  // Can modify
}

void process_container(const Container& container) {
    std::cout << "Processing const container" << std::endl;
    // container.add(42);  // Error! Cannot call non-const function
    container.print();     // Can call const function
}

int main() {
    Container container;
    container.add(1);
    container.add(2);
    container.add(3);
    
    // Non-const object
    std::cout << "=== Non-const object ===" << std::endl;
    process_container(container);
    container.print();
    
    // const object
    std::cout << "\n=== const object ===" << std::endl;
    const Container const_container = container;
    process_container(const_container);
    
    return 0;
}

Using const_cast

cpp
#include <iostream>
#include <string>

// Legacy API, accepts non-const pointer but doesn't actually modify data
void legacy_function(char* str) {
    std::cout << "Legacy function processing: " << str << std::endl;
    // Actually doesn't modify str's content
}

class CacheExample {
private:
    mutable std::string cached_result_;
    mutable bool cache_valid_;
    
public:
    CacheExample() : cache_valid_(false) {}
    
    // const function but needs to modify cache
    const std::string& expensive_computation() const {
        if (!cache_valid_) {
            // Use mutable to avoid const_cast
            cached_result_ = "Computed result";
            cache_valid_ = true;
        }
        return cached_result_;
    }
    
    // Bad practice: using const_cast
    const std::string& bad_computation() const {
        if (!cache_valid_) {
            // Remove const qualifier (dangerous!)
            auto* non_const_this = const_cast<CacheExample*>(this);
            non_const_this->cached_result_ = "Computed result";
            non_const_this->cache_valid_ = true;
        }
        return cached_result_;
    }
};

int main() {
    // Reasonable use of const_cast: calling legacy API
    const std::string message = "Hello, World!";
    
    // Need to call legacy function expecting non-const parameter
    // legacy_function(message.c_str()); // Error! const char* cannot convert to char*
    
    // Using const_cast (ensure function won't modify data)
    legacy_function(const_cast<char*>(message.c_str()));
    
    std::cout << "Message not modified: " << message << std::endl;
    
    // Cache example
    const CacheExample cache_obj;
    std::cout << "First call: " << cache_obj.expensive_computation() << std::endl;
    std::cout << "Second call: " << cache_obj.expensive_computation() << std::endl;
    
    return 0;
}

📋 Constant Best Practices

Constant Design Principles

cpp
#include <iostream>
#include <string>
#include <array>

// 1. Use meaningful constant names
namespace GameConfig {
    constexpr int MAX_PLAYERS = 4;
    constexpr double GRAVITY = 9.81;
    constexpr char SEPARATOR = '|';
    const std::string DEFAULT_PLAYER_NAME = "Player";
}

// 2. Group related constants together
class NetworkConstants {
public:
    static constexpr int DEFAULT_PORT = 8080;
    static constexpr int MAX_CONNECTIONS = 100;
    static constexpr double TIMEOUT_SECONDS = 30.0;
    static const std::string DEFAULT_HOST;
};

const std::string NetworkConstants::DEFAULT_HOST = "localhost";

// 3. Use constexpr instead of macros (when possible)
constexpr double calculate_circle_area(double radius) {
    return 3.14159 * radius * radius;
}

// 4. Constant array
constexpr std::array<int, 5> FIBONACCI = {1, 1, 2, 3, 5};

int main() {
    // Using namespace constants
    std::cout << "Max players: " << GameConfig::MAX_PLAYERS << std::endl;
    std::cout << "Gravity constant: " << GameConfig::GRAVITY << std::endl;
    
    // Using class constants
    std::cout << "Default port: " << NetworkConstants::DEFAULT_PORT << std::endl;
    std::cout << "Default host: " << NetworkConstants::DEFAULT_HOST << std::endl;
    
    // Compile-time computation
    constexpr double area = calculate_circle_area(5.0);
    std::cout << "Circle area: " << area << std::endl;
    
    // Constant array
    std::cout << "Fibonacci sequence: ";
    for (const auto& num : FIBONACCI) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Constant Selection Guide

cpp
#include <iostream>

// Choose the appropriate constant type
int main() {
    // 1. Simple values known at compile time: use constexpr
    constexpr int COMPILE_TIME_CONSTANT = 42;
    
    // 2. Values determined at runtime but don't change: use const
    const int runtime_input = []() {
        std::cout << "Enter a number: ";
        int value;
        std::cin >> value;
        return value;
    }();
    
    // 3. String literals: usually use const
    const std::string APPLICATION_NAME = "MyApp";
    
    // 4. Array sizes and template parameters: must use constexpr
    constexpr size_t ARRAY_SIZE = 10;
    std::array<int, ARRAY_SIZE> my_array;
    
    // 5. Constants in class: choose static const or static constexpr as needed
    class Example {
    public:
        static const std::string CLASS_NAME;        // Runtime constant
        static constexpr int CLASS_VERSION = 1;     // Compile-time constant
    };
    
    std::cout << "Compile-time constant: " << COMPILE_TIME_CONSTANT << std::endl;
    std::cout << "Runtime constant: " << runtime_input << std::endl;
    std::cout << "Application name: " << APPLICATION_NAME << std::endl;
    std::cout << "Array size: " << my_array.size() << std::endl;
    
    return 0;
}

Summary

C++ provides multiple methods for defining constants, each with its specific use cases:

Constant Type Comparison

TypeFeaturesUse Cases
constRuntime constant, immutableRuntime-determined immutable values
constexprCompile-time constant, usable in templatesValues known at compile time
#definePreprocessor substitutionConditional compilation, simple constants
enumNamed constant collectionGrouping related constants
static constClass-level constantsClass-related constants

Best Practices

  • Prefer constexpr over #define
  • Use const appropriately to ensure data is not accidentally modified
  • Organize related constants in namespaces or classes
  • Choose meaningful names for constants
  • Use const member functions where appropriate

Using constants correctly can:

  • Improve code safety
  • Enhance code readability
  • Enable compiler optimizations
  • Facilitate code maintenance

Content is for learning and research only.