Const Correctness: Writing Safer C++ Code

What is Const Correctness?

Const correctness is like putting safety locks on your code. It's a promise to the compiler (and other developers) about what your code will and won't modify. Think of it as labeling boxes "fragile" or "do not open" - it prevents accidents and makes intentions clear.

Const Variables and Parameters

The simplest use of const is to create read-only variables and function parameters that won't be modified.

graph TD A[Const Usage] --> B[Variables] A --> C[Parameters] A --> D[Return Values] B --> E[Local Constants] B --> F[Member Constants] C --> G[By Value] C --> H[By Reference] C --> I[By Pointer] style A fill:#3498db style C fill:#2ecc71 style H fill:#2ecc71

Basic Const Usage

// 1. Const Variables
void ConstVariables() {
    const int MaxHealth = 100;     // Cannot be modified
    const float Pi = 3.14159f;     // Compile-time constant
    
    // MaxHealth = 200;  // ERROR: Cannot modify const variable
    
    // Const with initialization
    const int Level = CalculateLevel();  // Set once, never changed
    
    // Arrays
    const int Scores[3] = {100, 95, 87};
    // Scores[0] = 99;  // ERROR: Cannot modify const array
}

// 2. Const Parameters - Pass by Value
void ProcessScore(const int score) {
    // score = 100;  // ERROR: Cannot modify const parameter
    std::cout << "Score: " << score << std::endl;
}

// 3. Const References - Very Common!
void PrintVector(const FVector& Location) {
    // Location.X = 0;  // ERROR: Cannot modify through const reference
    UE_LOG(LogTemp, Log, TEXT("Location: %s"), *Location.ToString());
}

// 4. Const Pointers - Multiple meanings!
void ConstPointers() {
    int value = 42;
    int other = 99;
    
    // Pointer to const int (data is const)
    const int* ptr1 = &value;
    // *ptr1 = 43;     // ERROR: Cannot modify data
    ptr1 = &other;     // OK: Can change pointer
    
    // Const pointer to int (pointer is const)
    int* const ptr2 = &value;
    *ptr2 = 43;        // OK: Can modify data
    // ptr2 = &other;  // ERROR: Cannot change pointer
    
    // Const pointer to const int (both const)
    const int* const ptr3 = &value;
    // *ptr3 = 43;     // ERROR: Cannot modify data
    // ptr3 = &other;  // ERROR: Cannot change pointer
}

// 5. Const References vs Copies
class ExpensiveObject {
    std::vector BigData;
public:
    // Bad: Unnecessary copy
    void ProcessSlow(ExpensiveObject obj) { }
    
    // Good: Const reference - no copy!
    void ProcessFast(const ExpensiveObject& obj) { }
};

Const Member Functions

Const member functions promise not to modify the object they're called on. They're like "read-only" operations on your class.

Implementing Const Member Functions

class Character {
private:
    float Health;
    int Level;
    mutable int CacheVersion;  // Can be modified in const functions
    
public:
    // Const member functions - promise not to modify object
    float GetHealth() const {
        // Health = 100;  // ERROR: Cannot modify in const function
        return Health;
    }
    
    int GetLevel() const { return Level; }
    
    // Const function returning const reference
    const FString& GetName() const { return Name; }
    
    // Non-const member functions - can modify object
    void TakeDamage(float Damage) {
        Health -= Damage;  // OK: Non-const function can modify
    }
    
    // Overloading based on const
    // Non-const version - returns modifiable reference
    FVector& GetPosition() { return Position; }
    
    // Const version - returns const reference
    const FVector& GetPosition() const { return Position; }
    
    // Mutable members can be modified in const functions
    void UpdateCache() const {
        CacheVersion++;  // OK: mutable member
    }
    
    // Const functions can call other const functions
    bool IsAlive() const {
        return GetHealth() > 0;  // OK: Calling const function
    }
    
    // ERROR: Const function cannot call non-const function
    void PrintStatus() const {
        // TakeDamage(0);  // ERROR: Cannot call non-const from const
    }
};

// Usage with const objects
void UseConstObject() {
    const Character ConstHero;
    Character MutableHero;
    
    // Const object can only call const functions
    float hp1 = ConstHero.GetHealth();     // OK
    // ConstHero.TakeDamage(10);            // ERROR
    
    // Non-const object can call both
    float hp2 = MutableHero.GetHealth();   // OK
    MutableHero.TakeDamage(10);            // OK
    
    // Const reference parameters
    void ProcessCharacter(const Character& ch) {
        float hp = ch.GetHealth();          // OK: const function
        // ch.TakeDamage(10);               // ERROR: non-const function
    }
}

Const with Pointers and References

Understanding const with pointers is like learning grammar rules - there are specific patterns that, once learned, make everything clear.

// The Const Pointer Rules - Read Right to Left!

// 1. Pointer to const data
const int* ptr1;        // ptr1 is a pointer to const int
int const* ptr2;        // Same as above (alternative syntax)
// Can change pointer, cannot change data

// 2. Const pointer to data  
int* const ptr3 = &value;  // ptr3 is a const pointer to int
// Cannot change pointer, can change data

// 3. Const pointer to const data
const int* const ptr4 = &value;  // ptr4 is a const pointer to const int
// Cannot change pointer or data

// Real-world Examples
class Game {
    // Return const pointer - caller can't modify data
    const Player* GetPlayer(int id) const {
        return Players[id];
    }
    
    // Return non-const pointer - caller can modify
    Player* GetMutablePlayer(int id) {
        return Players[id];
    }
    
    // Const reference - very common for parameters
    void AddScore(const FString& PlayerName, int Score) {
        // PlayerName cannot be modified
        Scores[PlayerName] += Score;
    }
    
    // Multiple const meanings in one declaration
    const int* const GetScores() const {
        // Returns const pointer to const data from const function!
        return &Scores[0];
    }
};

// Const with Smart Pointers
void SmartPointerConst() {
    // Const unique_ptr - pointer is const, data is not
    const std::unique_ptr p1 = std::make_unique();
    p1->SetHealth(100);     // OK: Can modify Player
    // p1.reset();          // ERROR: Cannot change pointer
    
    // Unique_ptr to const - data is const, pointer is not
    std::unique_ptr p2 = std::make_unique();
    // p2->SetHealth(100);  // ERROR: Cannot modify const Player
    p2.reset();             // OK: Can change pointer
    
    // Both const
    const std::unique_ptr p3 = std::make_unique();
    // p3->SetHealth(100);  // ERROR: Cannot modify data
    // p3.reset();          // ERROR: Cannot change pointer
}

Const Cast: Breaking Const (Carefully!)

Const cast is like having a master key - use it only when absolutely necessary and you know it's safe.

Using const_cast Safely

// Safe const_cast usage

// 1. Working with legacy C APIs
void LegacyAPI(char* str);  // Old C function that doesn't use const

void CallLegacyAPI(const std::string& str) {
    // We know LegacyAPI won't modify, but it needs non-const
    LegacyAPI(const_cast(str.c_str()));
}

// 2. Implementing const and non-const versions
class Container {
private:
    std::vector data;
    
public:
    // Non-const version
    int& operator[](size_t index) {
        return data[index];
    }
    
    // Const version - avoid code duplication
    const int& operator[](size_t index) const {
        // Call non-const version using const_cast
        return const_cast(this)->operator[](index);
    }
};

// 3. Mutable-like behavior before C++11
class OldCache {
private:
    mutable bool CacheValid;  // Modern way
    int CachedValue;
    
public:
    int GetValue() const {
        if (!CacheValid) {
            // Had to use const_cast before mutable keyword
            const_cast(this)->RecalculateCache();
        }
        return CachedValue;
    }
    
private:
    void RecalculateCache() { /* ... */ }
};

// DANGEROUS const_cast usage - AVOID!
void DangerousConstCast() {
    const int ConstValue = 42;
    int* ptr = const_cast(&ConstValue);
    *ptr = 99;  // UNDEFINED BEHAVIOR! Modifying const object
    
    // String literal - NEVER do this!
    const char* str = "Hello";
    char* mutable_str = const_cast(str);
    mutable_str[0] = 'h';  // CRASH! String literals are read-only
}

// When you SHOULD NOT use const_cast
void FixConstCorrectness(const Object& obj) {
    // BAD: Don't cast away const to "fix" design issues
    // const_cast(obj).Modify();  
    
    // GOOD: Fix the design instead
    // - Make the parameter non-const if modification is needed
    // - Use mutable for cache-like members
    // - Redesign the interface
}

Const in Real-World Code

Let's see how const correctness improves real game development code.

graph TD A[Const Best Practices] --> B[API Design] A --> C[Performance] A --> D[Thread Safety] B --> E[Clear Intent] B --> F[Prevent Misuse] C --> G[Compiler Optimizations] C --> H[Avoid Copies] D --> I[Immutable Data] D --> J[Read-Only Access] style A fill:#3498db style B fill:#2ecc71 style C fill:#f39c12 style D fill:#e74c3c

Game Development Example

// Well-designed game class with const correctness
class AGameCharacter : public AActor {
private:
    // Immutable configuration
    const float MaxHealth = 100.0f;
    const float MaxSpeed = 600.0f;
    
    // Mutable state
    float CurrentHealth;
    FVector Velocity;
    
    // Cached data (mutable for const functions)
    mutable bool bStatsCalculated;
    mutable FCharacterStats CachedStats;
    
public:
    // Const-correct getters
    float GetHealth() const { return CurrentHealth; }
    float GetMaxHealth() const { return MaxHealth; }
    float GetHealthPercent() const { return CurrentHealth / MaxHealth; }
    
    // Return const reference for efficiency
    const FVector& GetVelocity() const { return Velocity; }
    
    // Non-const reference for modification
    FVector& GetMutableVelocity() { return Velocity; }
    
    // Const-correct parameters
    void ApplyDamage(const FDamageEvent& DamageEvent) {
        // DamageEvent is read-only
        CurrentHealth -= DamageEvent.DamageAmount;
    }
    
    // Const member function with mutable cache
    const FCharacterStats& GetStats() const {
        if (!bStatsCalculated) {
            CalculateStats();  // Updates mutable cache
            bStatsCalculated = true;
        }
        return CachedStats;
    }
    
    // Comparison operators should be const
    bool operator==(const AGameCharacter& Other) const {
        return GetName() == Other.GetName();
    }
    
    // Iterator-style access
    TArray::TConstIterator GetWeaponsIterator() const {
        return Weapons.CreateConstIterator();
    }
    
private:
    void CalculateStats() const {
        // Can modify mutable members in const function
        CachedStats.Attack = CalculateAttack();
        CachedStats.Defense = CalculateDefense();
    }
};

// Using const in APIs
class UGameplayStatics {
public:
    // Const world parameter - won't modify world
    static AActor* SpawnActor(const UWorld* World, 
                             UClass* Class,
                             const FTransform& Transform);
    
    // Const array reference - efficient, read-only
    static float CalculateAverageDamage(const TArray& DamageValues);
    
    // Multiple const qualifiers for safety
    static const APlayerController* GetFirstPlayerController(const UWorld* World);
};

Common Const Pitfalls

Understanding common mistakes helps you write better const-correct code from the start.

// Pitfall 1: Forgetting const on getters
class Bad1 {
    int value;
public:
    int GetValue() { return value; }  // Should be const!
};

class Good1 {
    int value;
public:
    int GetValue() const { return value; }  // Correct
};

// Pitfall 2: Returning non-const reference to members
class Bad2 {
    std::vector data;
public:
    std::vector& GetData() const { return data; }  // ERROR!
};

class Good2 {
    std::vector data;
public:
    const std::vector& GetData() const { return data; }  // Correct
};

// Pitfall 3: Const object with non-const members
struct Bad3 {
    int* ptr;  // Pointer member
};

void UseBad3() {
    const Bad3 obj = {new int(42)};
    *obj.ptr = 99;  // Modifies pointed-to data despite const obj!
}

// Pitfall 4: Logical vs Physical constness
class String {
    char* data;
    mutable size_t cachedLength;
    
public:
    // Logically const - doesn't change visible state
    size_t Length() const {
        if (cachedLength == 0) {
            cachedLength = strlen(data);  // OK: mutable
        }
        return cachedLength;
    }
};

// Pitfall 5: Const in wrong place
const int* Function1();   // Returns pointer to const int
int* const Function2();   // Returns const pointer to int
int const* Function3();   // Same as Function1 (alternative syntax)

// Pitfall 6: East const vs West const
// Both are valid, be consistent!
const int* p1;  // West const (more common)
int const* p2;  // East const (arguably clearer)

Practice Exercise: const Correctness

Challenge: Make This Code const-correct

The following code lacks proper const usage. Add const wherever appropriate:

// Fix this code by adding const appropriately
class Player {
private:
    std::string name;
    int health;
    int maxHealth;
    std::vector inventory;
    Position currentPosition;
    bool isDead;
    
public:
    Player(std::string playerName, int startHealth) 
        : name(playerName), health(startHealth), maxHealth(startHealth) {}
    
    std::string GetName() { return name; }
    int GetHealth() { return health; }
    bool IsDead() { return health <= 0; }
    Position& GetPosition() { return currentPosition; }
    
    void TakeDamage(int damage) {
        health -= damage;
        if (health < 0) health = 0;
    }
    
    void AddItem(Item item) {
        inventory.push_back(item);
    }
    
    std::vector GetInventory() {
        return inventory;
    }
    
    void PrintStats() {
        std::cout << "Player: " << name << " HP: " << health << "/" << maxHealth;
    }
};

// Function that should use const
void DisplayPlayerInfo(Player player) {
    std::cout << player.GetName() << " has " << player.GetHealth() << " HP\n";
}

// Areas to fix:
// 1. Constructor parameters
// 2. Getter methods
// 3. Member functions that don't modify
// 4. Return types (copies vs references)
// 5. Function parameters

Key Takeaways

Remember: const is a promise. Make promises you can keep, and keep the promises you make. Your future self (and teammates) will thank you!