71. Mutable Counter in Const

In embedded systems, many driver APIs are marked const because calling them does not modify the device’s logical or externally observable state.

However, such functions may still update internal bookkeeping values, such as access counters or debug metrics.

These internal changes are allowed because they do not change the logical state of the object (e.g., hardware configuration, register contents, outputs). They only update metadata that is invisible to external users of the object.

Your task is to modify the class so that the logAccess() function can update an internal counter, even though both the function and the object are declared const.

What you must do:

  • The program calls logAccess() N times on a const object.
  • logAccess() must successfully increment the counter, even though it is a const function.
  • After N calls, getCount() must return N.
  • If N = 0, no calls should be recorded, and the output must be 0.
  • Modify only the class internals to make this work.

Although logAccess() modifies a variable, it only updates internal bookkeeping, not the logical external state, which makes this valid inside a const function.

Important:

Do not use incorrect approaches such as:

  • const_cast
  • Making the counter static
  • Removing the const keyword
  • Using global variables

These break const-correctness.

There is a correct and safe C++ mechanism to allow internal updates.

 

Example 

Input:

3

Output:

3

 

Constraints:

  • Keep logAccess() as a const function.
  • Keep the object in main() const.
  • Only modify the class internals to make the counter update possible.

 

 

 

 

Need Help? Refer to the Quick Guide below

In C++, a const member function (e.g., void read() const) promises not to modify any member variables of the object. The compiler enforces this strictly.

However, sometimes you need to modify a variable that does not affect the logical state of the object, such as a mutex lock, a usage counter, or a cached value.

The mutable keyword allows a specific member variable to be modified even inside a const function.

Syntax & Usage

1. Basic Usage (The Cache Example)

Imagine a sensor class where reading is const (logical state doesn't change), but you want to update a "last read timestamp" for debugging.

class Sensor {
private:
    int pin;
    mutable int last_access_time; // Can be changed in const functions

public:
    Sensor(int p) : pin(p), last_access_time(0) {}

    int read() const {
        // We are reading hardware, so logical state is const.
        // But we want to update the debug timestamp.
        
        last_access_time = get_system_time(); // ✅ Allowed because of 'mutable'
        
        return hardware_read(pin);
    }
};

2. Thread Safety (Mutexes)

This is the most common real-world use case. Locking a mutex changes the mutex's internal state, but it doesn't change the logical state of the data it protects.

class DataStore {
    mutable std::mutex mtx; // Mutex must be mutable
    int data;

public:
    int get_data() const {
        std::lock_guard<std::mutex> lock(mtx); // ✅ Modifies mtx, but that's okay
        return data;
    }
};

Logical vs. Bitwise Constness

Bitwise Const (Default)Logical Const (With mutable)
Compiler checks strictly.Developer promises logical consistency.
No bits in the object can change.Some bits (caches/locks) change, but the object "looks" the same.
Good for simple data structs.Essential for complex objects with internal management.

Relevance in Embedded/Firmware

1. Thread-Safe Getters

In firmware, you often have get_status() functions that must be thread-safe. To make them thread-safe, you need to lock a mutex or disable interrupts. Since get_status() should logically be const, you mark the mutex/lock as mutable to allow locking without removing the const qualifier from the API.

2. Lazy Initialization / Caching

Embedded systems often delay expensive calculations until needed.

class Config {
    mutable int cached_crc;
    mutable bool crc_valid = false;

public:
    int get_crc() const {
        if (!crc_valid) {
            cached_crc = calculate_expensive_crc(); // Modifies state lazily
            crc_valid = true;
        }
        return cached_crc;
    }
};

Common Pitfalls (Practical Tips)

PitfallDetails
❌ OveruseDon't use mutable just to cheat the compiler because you're too lazy to fix your design. Only use it for things that truly don't affect the object's "value."
❌ Thread Safety ConfusionJust because a variable is mutable doesn't make it thread-safe. You still need locks.
✅ Debug CountersIt is excellent for internal debug counters (e.g., mutable int access_count;) that track how many times a const function was called without changing the function signature.