Skillversitas

Storage Classes and Variable Scope in C++

A storage class determines a variable's lifetime (how long the variable lives), scope (where the variable can be accessed), and linkage (whether the variable can be accessed from other files). C++ has several storage classes: auto, register, static, extern, mutable, and thread_local.

auto (C++11)

In C++11, auto is used for type deduction — the compiler automatically determines the variable's type from its initialization. This replaced the old use of auto (which was the same as the default and was never used).

register (deprecated)

register is a hint for the compiler to store the variable in a CPU register. Since C++11, modern compilers ignore this hint and it is considered deprecated.

static

static has two different meanings depending on context:
- Local scope: A static local variable lives for the entire program (not just within the block), but can only be accessed within that block. Initialization is done only once.
- File scope: Sets internal linkage. A static function or variable at file scope can only be accessed from that file (internal linkage).

extern

extern declares a variable that is defined elsewhere. It tells the compiler: "this variable exists, its definition is in another file/location." Useful for global variables shared between files.

mutable

mutable allows a struct/class member to be modified even if the object is const. Useful for caching or logging that does not change the object's logical state.

thread_local (C++11)

thread_local gives a variable thread storage duration. Each thread has its own independent copy of the variable. Useful for thread-specific data without race conditions.

Lifetime vs Visibility

  • Lifetime: How long a variable holds its value in memory. It can be static (the whole program), automatic (block scope), or dynamic (heap via new).
  • Visibility (scope): Where in the code the variable can be accessed. It can be global, local, or block-scoped.

Code Example

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
using namespace std;

// Extern: defined elsewhere (could be in another file)
extern int sharedData;

// File-scope static: can only be accessed in this file
static int fileStatic = 100;
static void fileFunction() {
    cout << "This function is internal, only for this file" << endl;
}

// Mutable struct
struct CachedData {
    mutable int hitCount = 0;
    string data;

    string getData() const {
        hitCount++; // allowed because mutable
        return data;
    }
};

// Thread-local
thread_local int threadSpecific = 0;

void autoFunction() {
    // auto type deduction
    auto x = 42;          // int
    auto y = 3.14;        // double
    auto z = "Hello";     // const char*
    auto w = string("Hi"); // string

    cout << "auto x: " << x << " (" << typeid(x).name() << ")" << endl;
    cout << "auto y: " << y << endl;
    cout << "auto z: " << z << endl;
}

void counterStatic() {
    static int count = 0; // initialized only once
    count++;
    cout << "Function called " << count << " times" << endl;
}

void demonstrasiScope() {
    int lokal = 10; // automatic storage duration

    {
        int dalamBlok = 20; // block scope
        cout << "lokal: " << lokal << ", dalamBlok: " << dalamBlok << endl;
    }
    // dalamBlok cannot be accessed here

    static int staticLokal = 0; // static local: lives forever
    staticLokal++;
    cout << "staticLokal: " << staticLokal << endl;
}

void threadFunction(int id) {
    threadSpecific = id * 100;
    cout << "Thread " << id << ": threadSpecific = " << threadSpecific << endl;
}

int main() {
    // auto
    cout << "--- auto ---" << endl;
    autoFunction();

    // Static local
    cout << "\n--- Static Local ---" << endl;
    counterStatic(); // count = 1
    counterStatic(); // count = 2
    counterStatic(); // count = 3

    // Scope demonstration
    cout << "\n--- Scope ---" << endl;
    demonstrasiScope();
    demonstrasiScope(); // staticLokal continues from 1

    // Extern
    cout << "\n--- Extern ---" << endl;
    sharedData = 42;
    cout << "sharedData: " << sharedData << endl;

    // Mutable
    cout << "\n--- Mutable ---" << endl;
    CachedData cache = {"sensitive data"};
    const CachedData& ref = cache;
    cout << "Data: " << ref.getData() << endl;
    cout << "Data: " << ref.getData() << endl;
    cout << "Hit count: " << ref.hitCount << endl;

    // Thread-local
    cout << "\n--- Thread Local ---" << endl;
    thread t1(threadFunction, 1);
    thread t2(threadFunction, 2);
    t1.join();
    t2.join();

    // Static global
    cout << "\n--- Static Global ---" << endl;
    cout << "fileStatic: " << fileStatic << endl;
    fileFunction();

    // Lifetime demonstration
    cout << "\n--- Lifetime ---" << endl;
    int* ptr;
    {
        int dynamic = 42; // automatic: dies at the end of the block
        ptr = new int(100); // dynamic: lives until delete
    }
    // dynamic has already died, but *ptr is still alive
    cout << "*ptr = " << *ptr << endl;
    delete ptr;

    // Static vs auto
    cout << "\n--- Static vs Auto ---" << endl;
    for (int i = 0; i < 3; i++) {
        int autoVar = 0; // re-initialized every iteration
        static int staticVar = 0; // initialized only once
        autoVar++;
        staticVar++;
        cout << "Iterasi " << i << ": auto=" << autoVar
             << ", static=" << staticVar << endl;
    }

    return 0;
}

// Extern definition (could be in another file, but here for demonstration)
int sharedData = 0;

Code Explanation

  • auto x = 42; — The compiler determines that x is of type int from the initialization value 42. Saves writing the type explicitly, especially for complex types such as iterators or templates.
  • auto y = 3.14; — y is automatically double. auto z = "Hello" is automatically const char* (not string).
  • static int count = 0; — A static local variable. Initialized only once the first time the function is called. The value persists across function calls. Useful for counters, caching, or state that needs to be remembered.
  • demonstrasiScope() called twice — staticLokal is initialized once (initial value 0) and increments on every call. Output: 1, then 2.
  • extern int sharedData; — Declares a variable defined elsewhere. Here, sharedData is defined at the end of the file. Useful for sharing data between files.
  • static int fileStatic — Static at file scope. Internal linkage. Can only be accessed from this file. Another function in a different file can have its own fileStatic variable without conflict.
  • mutable int hitCount — Inside a const method, but hitCount can still be modified because of mutable. Useful for logging or memoization that does not affect the logical state.
  • thread_local int threadSpecific — Each thread has its own copy. Thread 1 changes threadSpecific to 100, thread 2 to 200. They do not affect each other.
  • int autoVar vs static int staticVar inside a loop — autoVar is re-initialized every iteration (always 1). staticVar is initialized only once (keeps increasing: 1, 2, 3).
  • int* ptr = new int(100) — Dynamic allocation. Lives until delete ptr. Different from automatic variables that die when leaving scope.

Everyday Use Cases

Singleton pattern: A static local variable inside a function ensures the object is created only once. Every call gets the same object.

Database& getDB() {
    static Database instance; // created once
    return instance;
}

Thread pool: thread_local stores a worker ID or thread-specific context. Each thread has its own data without a race condition.

Global counter: extern allows several source files to share a counter variable. File A declares extern int totalRequest;, File B defines int totalRequest = 0;.

Caching: mutable on a class member allows caching in a const method. The method does not change the logical state but can store previously computed results.

Conclusion

Storage classes control a variable's lifetime and visibility. auto (C++11) is for type deduction. A local static persists for the entire program. extern is for sharing variables between files. mutable allows modification on const objects. thread_local (C++11) is for thread-specific data. Understand the difference between lifetime and scope: a variable can live a long time but have limited scope, or vice versa. Choosing the right storage class results in an efficient, maintainable program.