#041 – Global mutable variables do not belong in any program

A variable defined in the global namespace can be accessed ANYWHERE in the source code. If that sounds powerful, it’s because it is. This accessibility is the source of both its appeal and its danger.

  • Mutable global variables should be avoided unless their immutable
  • In avoiding global variables, consider how it can be transformed into a parameter instead.

A global variable has external linkage.

It is accessible from anywhere in the program.

#include <iostream>

int health {100};

int main() {
	std::cout << "I have global scope: " << health;
}

Why mutable global variables cause problems

Consider a program that builds a final price. addItem(), applyDiscount() manipulate the global variable g_totalPrice.

#include <iostream>

double g_totalPrice = 0.0;

void addItem(double price) {
    g_totalPrice += price; // Operate on the global variable
}

void applyDiscount() {
    if (g_totalPrice > 100.0) { // Operate on the global variable
        g_totalPrice -= 10.0;
    }
}

void addTax() {
    g_totalPrice *= 1.05; // 5% tax
}

int main() {
    addItem(50.0);
    addItem(60.0);
    
    applyDiscount();
    addTax();
    
    std::cout << "Total: $" << g_totalPrice << "\\n";
}

This seems fairly harmless. g_totalPrice is used as the glue between the program logic. However, after you scrutinize it, it quickly falls apart.

  1. What is applyDiscount() discounting?
    1. applyDiscount() takes no arguments and returns nothing, yet it mutates state. The reader is forced to inspect the function body to discover what it actually does and its side effects. ****
  2. If addTax() is relocated to run before applyDisconut() then the entire logic fails. Thus, the program assumes a silent, implicit order to the function calls. However, since this isn’t enforced, another programmer has the liberty to swap the call order. The program breaks.
  3. If we want to write a test for addTax(), we have to reset the global g_totalPrice to a specific value before every single test case, or tests will interfere with each other.

Thus, having functions dependent on global variables makes it difficult to understand the logic and introduces many opportunities for error.

The fix

Transform the global variable into a local variable. Refactor the functions to accept and return a value, thereby conveying the stream of data.

#include <iostream>

double calculateDiscount(double currentTotal) {
    if (currentTotal > 100.0) {
        return currentTotal - 10.0;
    }
    return currentTotal;
}

double calculateTax(double currentTotal) {
    return currentTotal * 1.05;
}

int main() {
    double Total = 0.0;

    Total += 50.0; // Purchased a shirt
    Total += 60.0; // Purchase shoes

    local = calculateDiscount(local);
    local = calculateTax(loca);

    std::cout << "Total: $" << local << "\\n";
}

Each function is now a pure transformation: input in, result out. Order of operations is explicit, side effects are gone

When global variables are acceptable

In my Budget Tracker program, I paste the UI in the global namespace. Since its, constexpr, it’s implicitly const. This makes it immutable. Thus, it can reside in the global namespace and I don’t have to worry about its contents being manipulated.

constexpr std::string_view menu = R"(
=== MONTHLY BUDGET TRACKER ===
--- Main Menu ---
1. Enter expenses for a month
2. View all monthly expenses
3. Calculate total yearly spend
4. Identify month with highest/lowest spending
5. Set budget goals
6. Compare against monthly budget
7. Exit
)";

A menu string, a mathematical constant, a configuration table — these are ideal candidates for global scope since their immutable.

Takeaway

The danger is not the global keyword; it is shared mutable state.

If a value is mutable, give it a clear owner and pass it where it is needed. Otherwise, it can reside in the global namespace.