Ready to practice?
Sign up to access interactive coding exercises and track your progress.
The Problems with Global Mutable State
Recognize the dangers of mutable global state and learn safer alternatives.
Why (Non-Const) Global Variables Are Evil
If you were to ask a veteran programmer for one piece of advice on good programming practices, after some thought, the most likely answer would be, "Avoid global variables!". And with good reason: global variables are one of the most historically abused concepts in the language. Although they may seem harmless in small academic programs, they are often problematic in larger ones.
New programmers are often tempted to use lots of global variables, because they are easy to work with, especially when many calls to different functions are involved (passing data through function parameters is a pain). However, this is generally a bad idea. Many developers believe non-const global variables should be avoided completely.
But before we go into why, we should make a clarification. When developers tell you that global variables are evil, they're usually not talking about all global variables. They're mostly talking about non-const global variables.
Why (non-const) global variables are evil
By far the biggest reason non-const global variables are dangerous is because their values can be changed by any function that is called, and there is no easy way for the programmer to know that this will happen. Consider the following program:
#include <iostream>
int g_systemState; // declare global variable (will be zero-initialized by default)
void modifySystem()
{
g_systemState = 5; // set the global g_systemState variable to 5
}
int main()
{
g_systemState = 1; // note: this sets the global g_systemState variable to 1. It does not declare a local g_systemState variable!
modifySystem();
// Programmer still expects g_systemState to be 1
// But modifySystem changed it to 5!
if (g_systemState == 1)
{
std::cout << "System is safe.\n";
}
else
{
std::cout << "Critical system failure detected...\n";
}
return 0;
}
Note that the programmer set variable g_systemState to 1, and then called modifySystem(). Unless the programmer had explicit knowledge that modifySystem() was going to change the value of g_systemState, he or she was probably not expecting modifySystem() to change the value. Consequently, the rest of main() doesn't work like the programmer expects (and a critical system failure is detected).
In short, global variables make the program's state unpredictable. Every function call becomes potentially dangerous, and the programmer has no easy way of knowing which ones are dangerous and which ones aren't. Local variables are much safer because other functions cannot affect them directly.
There are plenty of other good reasons not to use non-const globals.
With global variables, it's not uncommon to find a piece of code that looks like this:
void processRequest()
{
// useful code
if (g_systemState == 7)
{
// do something critical
}
}
After debugging, you determine that your program isn't working correctly because g_systemState has value 2, not 7. How do you fix it? Now you need to find all of the places g_systemState could possibly be set to 2, and trace through how it got set in the first place. It's possible this may be in a totally unrelated piece of code.
One of the key reasons to declare local variables as close to where they are used as possible is because doing so minimizes the amount of code you need to look through to understand what the variable does. Global variables are at the opposite end of the spectrum -- because they can be accessed anywhere, you might have to look through the entire program to understand their usage. In small programs, this might not be an issue. In large ones, it will be.
For example, you might find g_systemState is referenced 537 times in your program. Unless g_systemState is well documented, you'll potentially have to look through every use of g_systemState to understand how it's being used in different cases, what its valid values are, and what its overall function is.
Global variables also make your program less modular and less flexible. A function that utilizes nothing but its parameters and has no side effects is perfectly modular. Modularity helps both in understanding what a program does, as well as with reusability. Global variables reduce modularity significantly.
In particular, avoid using global variables for important "decision-point" variables (e.g., variables you'd use in a conditional statement, like variable g_systemState in the example above). Your program isn't likely to break if a global variable holding an informational value changes (e.g., like the user's name). It is much more likely to break if you change a global variable that impacts how your program actually functions.
Use local variables instead of global variables whenever possible.
Initialization of static variables (which includes global variables) happens as part of program startup, before execution of the main function. This proceeds in two phases.
The first phase is called static initialization. Static initialization proceeds in two phases:
- Global variables with constexpr initializers (including literals) are initialized to those values. This is called constant initialization.
- Global variables without initializers are zero-initialized. Zero-initialization is considered to be a form of static-initialization since
0is a constexpr value.
The second phase is called dynamic initialization. This phase is more complex and nuanced, but the gist of it is that global variables with non-constexpr initializers are initialized.
Here's an example of a non-constexpr initializer:
int initialize()
{
return 42;
}
int g_value{initialize()}; // non-constexpr initialization
Within a single file, for each phase, global variables are generally initialized in order of definition (there are a few exceptions to this rule for the dynamic initialization phase). Given this, you need to be careful not to have variables dependent on the initialization value of other variables that won't be initialized until later. For example:
#include <iostream>
int initializeFirst(); // forward declaration
int initializeSecond(); // forward declaration
int g_first{initializeFirst()}; // g_first is initialized first
int g_second{initializeSecond()};
int initializeFirst()
{
return g_second; // g_second isn't initialized when this is called
}
int initializeSecond()
{
return 42;
}
int main()
{
std::cout << g_first << ' ' << g_second << '\n';
}
This prints:
0 42
Much more of a problem, the order in which static objects are initialized across different translation units is ambiguous.
Given two files, config.cpp and database.cpp, either could have its global variables initialized first. If some variable with static duration in config.cpp is initialized with a static duration variable defined in database.cpp, there's a 50% chance that the variable in database.cpp won't be initialized yet.
The ambiguity in the order that objects with static storage duration in different translation units are initialized is often called the [static initialization order fiasco](https://en.cppreference.com/w/cpp/language/siof).
Avoid initializing objects with static duration using other objects with static duration from a different translation unit.
Dynamic initialization of global variables is also susceptible to initialization order issues and should be avoided whenever possible.
So what are very good reasons to use non-const global variables?
There aren't many. In most cases, using local variables and passing them as arguments to other functions is preferable. But in some cases, judicious use of non-const global variables can actually reduce program complexity, and in these rare cases, their use may be better than the alternatives.
A good example is a log file, where you can dump error or debug information. It probably makes sense to define this as a global, because you're likely to only have one such log in a program and it will likely be used everywhere in your program. Another good example would be a random number generator (we show an example of this in the Global random numbers lesson).
For what it's worth, the std::cout and std::cin objects are implemented as global variables (inside the std namespace).
As a rule of thumb, any use of a global variable should meet at least the following two criteria: There should only ever be one of the thing the variable represents in your program, and its use should be ubiquitous throughout your program.
Many new programmers make the mistake of thinking that something can be implemented as a global because only one is needed right now. For example, you might think that because you're implementing a single player game, you only need one player. But what happens later when you want to add a multiplayer mode (versus or hotseat)?
Protecting yourself from global destruction
If you do find a good use for a non-const global variable, a few useful bits of advice will minimize the amount of trouble you can get into. This advice isn't only for non-const global variables, but can help with all global variables.
First, prefix all non-namespaced global variables with "g" or "g_", or better yet, put them in a namespace (discussed in the User-defined namespaces and the scope resolution operator lesson), to reduce the chance of naming collisions.
For example, instead of:
#include <iostream>
constexpr double acceleration{9.8}; // risk of collision with some other global variable named acceleration
int main()
{
std::cout << acceleration << '\n'; // unclear if this is a local or global variable from the name
return 0;
}
Do this:
#include <iostream>
namespace physics
{
constexpr double acceleration{9.8}; // will not collide with other global variables named acceleration
}
int main()
{
std::cout << physics::acceleration << '\n'; // clear this is a global variable (since namespaces are global)
return 0;
}
Second, instead of allowing direct access to the global variable, it's a better practice to "encapsulate" the variable. Make sure the variable can only be accessed from within the file it's declared in, e.g., by making the variable static or const, then provide external global "access functions" to work with the variable. These functions can ensure proper usage is maintained (e.g., do input validation, range checking, etc...). Also, if you ever decide to change the underlying implementation (e.g., move from one database to another), you only have to update the access functions instead of every piece of code that uses the global variable directly.
For example, instead of this:
settings.cpp:
namespace settings
{
extern const double gravity{9.8}; // has external linkage, can be accessed by other files
}
main.cpp:
#include <iostream>
namespace settings
{
extern const double gravity; // forward declaration
}
int main()
{
std::cout << settings::gravity << '\n'; // direct access to global variable
return 0;
}
Do this:
settings.cpp:
namespace settings
{
constexpr double gravity{9.8}; // has internal linkage, is accessible only within this file
}
double getGravity() // has external linkage, can be accessed by other files
{
// We could add logic here if needed later
// or change the implementation transparently to the callers
return settings::gravity;
}
main.cpp:
#include <iostream>
double getGravity(); // forward declaration
int main()
{
std::cout << getGravity() << '\n';
return 0;
}
A reminder
Global const variables have internal linkage by default, gravity doesn't need to be static.
Third, when writing an otherwise standalone function that uses the global variable, don't use the variable directly in your function body. Pass it in as an argument instead. That way, if your function ever needs to use a different value for some circumstance, you can simply vary the argument. This helps maintain modularity.
Instead of:
#include <iostream>
namespace physics
{
constexpr double acceleration{9.8};
}
// This function is only useful for calculating your instant velocity based on the global acceleration
double calculateVelocity(int seconds)
{
return physics::acceleration * seconds;
}
int main()
{
std::cout << calculateVelocity(3) << '\n';
return 0;
}
Do this:
#include <iostream>
namespace physics
{
constexpr double acceleration{9.8};
}
// This function can calculate the instant velocity for any acceleration value (more useful)
double calculateVelocity(int seconds, double acceleration)
{
return acceleration * seconds;
}
int main()
{
std::cout << calculateVelocity(3, physics::acceleration) << '\n'; // pass our constant to the function as a parameter
return 0;
}
A C++ joke
What's the best naming prefix for a global variable?
Answer: //
This joke is worth all the comments.
Summary
Why non-const globals are problematic: Their values can be changed by any function, making program state unpredictable. Every function call becomes potentially dangerous since you can't easily know which functions modify global state.
Debugging difficulties: When a global has an unexpected value, you must search the entire program to find where it was modified. In large programs, this can be extremely time-consuming.
Reduced modularity: Functions that use global variables are less modular and harder to reuse. They have implicit dependencies that aren't visible in their signatures.
Initialization order issues: Static initialization across translation units happens in an ambiguous order, potentially causing bugs if one global depends on another from a different file. This is called the static initialization order fiasco.
When they might be acceptable: Only when there should truly be exactly one instance throughout the program and its use is ubiquitous. Examples: logging systems, random number generators, or singleton-like resources.
Protection strategies: If you must use non-const globals, (1) prefix them with "g_" or put them in namespaces, (2) encapsulate them behind access functions rather than allowing direct access, and (3) pass them as function parameters rather than accessing them directly within function bodies.
The fundamental issue with non-const global variables is that they create hidden dependencies and mutable shared state, both of which make programs harder to understand, debug, and maintain. Local variables with explicit parameter passing is almost always the better choice.
The Problems with Global Mutable State - Quiz
Test your understanding of the lesson.
Practice Exercises
Refactoring Global Variables
Refactor code that uses problematic global variables into safer alternatives using function parameters.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!