Saw this on Twitter and thought about it:
I try not to read comments (When I do, one of my glands produces bleach and pumps it to my brain), but I couldn’t resist. What were other people saying? As with most cesspools of opinions, the responses were polarizing: Global variables are evil or unavoidable. Some people claim a singleton mitigate issues with global variables, but we know the truth. The issues are still there. My take on this topic is also trying to crawl out of Bias Swamp, but I get more than 140 characters to do it.
Global Variables in Real Life
My original analogy involved returning an item to Target, but I wanted something more relatable to people my age and more mundane. Assuming you have a driver’s license, you’ll know what I’m talking about…
Congratulations: You passed your driving test. You bested the mean-old-man, destined to fail you, and stinking up your mom’s SUV. Now take a number and wait for it to be called. Everyone at the DMV gets a number from the machine. When a number is taken, then next one gets incremented. There’s your global variable.
What Problems will a Global Variable Introduce?
My local DMV only has one person handing out numbers. Consequently, there is a line to get a number. Once you get your number, you then must wait for it to be called. (Can you believe I only waited 30 minutes once?) Only then do you face the final boss and accomplish what you left your house for.
What if there were two greeters handing out numbers for people who walk through the door. It would stand to reason that the line may be shorter. Get your number from one of them and take a seat.
Your number is called but there’s someone else at the counter with the same number as you! There’s the problem no one mentions with global variables.
Global variables are a shortcut to share information across a program’s modules. Often, these modules don’t need to know about each other, but must share some common feature. Here’s a concrete example: Your program assigns an ID to every window you create. Why not use a static class that hands out IDs? When two windows request a unique ID from different threads, you have a race condition. You could wrap calls to “IDManager.GetNextID()” with a mutex, but you’ve created an unnecessary bottleneck.
Avoiding a global variable
I can think of a way to avoid a singleton for unique IDs:
Pass the ID to the window’s constructor. This is pretty straight forward. Program starts in “main()”, creates an IDManager and before creating a window, call idManager.GetNextID() and pass it to the window’s constructor. Ta-da, minimal coupling and no global variable. You cannot get new IDs across multiple threads without some locking, but since your IDManager must be instantiated (it isn’t static), it cannot live across multiple threads unless you make a copy of. (At least, its not safe to pass references across threads)
The operating system I wrote at university uses some singletons. If I had time to think about the architecture more, I could have avoided some of them. My memory manager was a singleton, which meant in order to test anything that allocated memory, my testing apparatus needed a singleton. Memory would be corrupted if two things tried to allocate memory at the same time.
A better solution would to require a kernel module to have its own memory allocator. This would allow two major benefits:
- No global variables. A module can allocate memory for its member variables without reaching outside its own scope.
- You can enforce the region of memory a module may operate in. If a module doesn’t need much memory, give it a small allocator. Your keyboard driver shouldn’t need more than 1KB of memory. Your graphics driver or thread scheduler may need much more though. Tune the allocators for different uses.
There would be some drawbacks though:
- Assigning memory regions would need to be coordinated; multiple kernel modules could not be created simultaneously without locking. I’m sure you’d hate for the mouse driver and thread scheduler to share the same region.
- You cannot use the “new” operator or “malloc” to allocate memory. “new” must be overridden globally, which requires a global memory allocator. “new” could be overridden by each class, except it is a free function so it still needs a memory allocator outside the class scope; meaning global scope.
Singleton may be Unavoidable
I’ve read one good case for a singleton, but I’m still not convinced. A singleton would be useful for playing sounds. Since, I’m involved in game systems programming right now, I’ll use an audio engine as the example. Your computer only outputs to one audio source at a time. Stands to reason that there should be only one audio engine in a game then. Why not make it a singleton so everything that wants to play sound can reach into the singleton and queue up some explosions. Sounds reasonable. Except, there is a reason mom put the cookie jar on the top shelf. My input system shouldn’t play sounds. The achievement tracker shouldn’t play sounds.
I see the same logic used for the graphics context. The DirectX12 MiniEngine uses a singleton to wrap the DirectX devices. Granted, the MiniEngine is more impressive that what I have, but I don’t have any global variables in my project. I wrapped some functionality into a GraphicsDevice class. Everything that needs to be aware of the graphics device uses a shared pointer. I even “delete”-ed the GraphicsDevice’s copy constructor. Using a shared_ptr is nice because you don’t have to track the lifetime of the pointer. Every class in my engine that needs information from the GraphicsDevice uses a shared_ptr. No global variable and you can have multiple GraphicsDevices, each one bound to a different GPU.
Wrap Up
I didn’t say global variables are evil, but I haven’t found one necessary yet.