Static Local Variables

The static keyword is notoriously confusing in C++ because it means different things in different contexts.

Previously, we covered that global variables have static duration—created when the program starts and destroyed when it ends.

We also discussed how static gives global identifiers internal linkage, restricting their use to the file where they're defined.

This lesson explores using static with local variables.

Static Local Variables

In earlier chapters, you learned that local variables have automatic duration by default: created at their definition point and destroyed when their enclosing block exits.

The static keyword changes a local variable's duration from automatic to static. This means the variable is created at program start and destroyed at program end (like a global variable). Consequently, static variables retain their values even after going out of scope.

The difference between automatic and static duration is best shown through examples.

Automatic duration (default):

#include <iostream>

void trackScore()
{
    int score{ 100 };
    score += 50;
    std::cout << score << '\n';
}

int main()
{
    trackScore();
    trackScore();
    trackScore();

    return 0;
}

Each trackScore() call creates score, initializes it to 100, increments it to 150, prints 150, then destroys it when the function ends. This program outputs:

150
150
150

Now consider the static duration version—the only change is adding static:

Static duration (using static keyword):

#include <iostream>

void trackScore()
{
    static int s_score{ 100 };
    s_score += 50;
    std::cout << s_score << '\n';
}

int main()
{
    trackScore();
    trackScore();
    trackScore();

    return 0;
}

Because s_score is declared static, it's created at program start.

Static local variables with zero-initialization or constexpr initializers can be initialized at program start. Static local variables with no initializer or non-constexpr initializers are zero-initialized at program start, then reinitialized the first time the definition is encountered. Subsequent calls skip the definition, preventing re-initialization. Because they have static duration, static local variables without explicit initialization are zero-initialized by default.

Since s_score has constexpr initializer 100, it initializes at program start.

When s_score goes out of scope at function end, it's not destroyed. Each subsequent trackScore() call finds s_score with whatever value we left it at previously. This program outputs:

150
200
250

Static local variables are useful when you need a local variable to remember its value across function calls.

Initialize your static local variables. Static local variables initialize only the first time code executes, not on subsequent calls.

Just as we prefix global variables with g_, it's common to prefix static (static duration) local variables with s_.

ID Generation

One common use for static duration local variables is unique ID generation. Imagine a program with many similar objects (like a game with multiple enemy types, or a graphics application rendering many shapes). Debugging issues with specific objects becomes difficult when you cannot distinguish between them. Giving each object a unique ID upon creation makes differentiation easier.

Generating unique IDs is straightforward with static duration local variables:

int generateEnemyID()
{
    static int s_enemyID{ 0 };
    return s_enemyID++;
}

The first call returns 0. The second returns 1. Each call returns a number one higher than before. Assign these as unique IDs to your objects. Because s_enemyID is local, other functions cannot tamper with it.

Static variables offer global variable benefits (persistence until program end) while limiting visibility to block scope. This makes them easier to understand and safer to use.

A static local variable has block scope like a local variable, but lifetime until program end like a global variable.

Static Local Constants

Static local variables can be const or constexpr. One good use for const static local variables: when a function needs a const value, but creating or initializing it is expensive (like loading configuration from a database). A normal local variable would be created and initialized on every function call. With a const/constexpr static local variable, you create and initialize the expensive object once, then reuse it on subsequent calls.

Static local variables are best used to avoid expensive local object initialization each time a function is called.

Don't Use Static Local Variables to Alter Flow

Consider this code:

#include <iostream>

int getUserAge()
{
    static bool s_isFirstRequest{ true };

    if (s_isFirstRequest)
    {
        std::cout << "Please enter your age: ";
        s_isFirstRequest = false;
    }
    else
    {
        std::cout << "Please enter another age: ";
    }

    int age{};
    std::cin >> age;
    return age;
}

int main()
{
    int age1{ getUserAge() };
    int age2{ getUserAge() };

    std::cout << "Ages: " << age1 << " and " << age2 << '\n';

    return 0;
}

Sample output:

Please enter your age: 25
Please enter another age: 30
Ages: 25 and 30

This works, but the static local variable makes it harder to understand. Reading main() without examining getUserAge() implementation gives no indication the two calls behave differently. Yet they do, which is confusing when the difference is more subtle than a changed prompt.

Imagine pressing the "Cook" button on your microwave and it heats for 2 minutes. You watch a bird outside for a moment. When you return, your food is cold, so you press "Cook" again—but this time it only heats for 10 seconds. You'd think "I changed nothing and now it's broken" or "It worked last time." Same actions should produce consistent results. This applies to functions too.

Suppose we expand the program to calculate age differences:

First person
Please enter your age: 25
Please enter another age: 30
Ages: 25 and 30
Age difference: 5

Second person
Please enter your age: 45
Please enter another age: 50
Ages: 45 and 50
Age difference: 5

We might try reusing getUserAge() for the second pair:

int main()
{
    std::cout << "First person\n";
    int age1{ getUserAge() };
    int age2{ getUserAge() };
    std::cout << "Ages: " << age1 << " and " << age2 << '\n';
    std::cout << "Age difference: " << (age2 - age1) << '\n';

    std::cout << "\nSecond person\n";
    int age3{ getUserAge() };
    int age4{ getUserAge() };
    std::cout << "Ages: " << age3 << " and " << age4 << '\n';
    std::cout << "Age difference: " << (age4 - age3) << '\n';

    return 0;
}

But the output is:

First person
Please enter your age: 25
Please enter another age: 30
Ages: 25 and 30
Age difference: 5

Second person
Please enter another age: 45
Please enter another age: 50
Ages: 45 and 50
Age difference: 5

(The third-to-last line says "another age" instead of "your age")

getUserAge() is not reusable because its internal state (s_isFirstRequest) cannot be reset externally. Although our initial program worked perfectly, the static local variable prevents function reuse later.

One better approach: pass s_isFirstRequest as a parameter, letting callers choose which prompt appears:

#include <iostream>

constexpr bool g_firstRequest{ true };

int getUserAge(bool isFirstRequest)
{
    if (isFirstRequest)
    {
        std::cout << "Please enter your age: ";
    }
    else
    {
        std::cout << "Please enter another age: ";
    }

    int age{};
    std::cin >> age;
    return age;
}

int main()
{
    int age1{ getUserAge(g_firstRequest) };
    int age2{ getUserAge(!g_firstRequest) };

    std::cout << "Ages: " << age1 << " and " << age2 << '\n';

    return 0;
}

Non-const static local variables should only be used if the variable is truly unique throughout your program and won't ever need resetting.

Const static local variables are generally acceptable.

Non-const static local variables should generally be avoided. If you use them, ensure the variable never needs resetting and isn't used to alter program flow.

An even more reusable solution: change the bool parameter to std::string_view, letting callers pass the prompt text directly.

For cases needing multiple instances of a non-const variable that remembers its value (like multiple ID generators), a functor is a good solution.

Quiz Time

Question #1

What effect does the static keyword have on a global variable? What effect does it have on a local variable?

When applied to a global variable, static defines it with internal linkage, preventing it from being exported to other files.

When applied to a local variable, static defines it with static duration, meaning it's created once and persists until program end, retaining its value across function calls.

Summary

Static local variables: Local variables with static duration that persist until program end. Declared using the static keyword. Retain their values across function calls.

Duration vs scope: Static local variables have block scope like normal local variables but static duration like global variables. They're visible only within their block but exist for the program's entire lifetime.

Initialization: Static local variables with constexpr initializers are initialized at program start. Those with non-constexpr initializers are zero-initialized at program start, then initialized the first time execution reaches their definition. Subsequent function calls skip re-initialization.

Default initialization: Static local variables without explicit initialization are zero-initialized by default (unlike automatic local variables which are uninitialized).

Common use - ID generation: Useful for generating unique IDs since the counter persists across function calls while remaining protected from external modification.

Common use - expensive initialization: Good for const values that are expensive to create/initialize. Initialize once on first call, reuse on subsequent calls.

Avoid for control flow: Don't use static local variables to change function behavior between calls. This makes functions harder to understand and reuse. Prefer passing information as parameters.

Naming convention: Prefix static local variables with "s_" to indicate static duration.

Best practices: Const static local variables are generally acceptable. Non-const static local variables should be avoided unless the variable is truly unique and never needs resetting.

Static local variables provide a middle ground between global and local variables: they have global lifetime but local visibility, making them safer than globals while still providing persistence across function calls.