> Your program must be really small and scoped for this to make sense.
to me suggests that it's not really global state if your program has to stay small and scoped. In a sense, your program has just become the context boundary for the state, instead of a function, or class, or database.
I realise that this line of argument effectively leads to the idea that no state is global, but perhaps that gives us a better way to understand the claim that 'global variables can work', which they undoubtedly can. It's fine for a program (or a thread, as in the original article) to be the context which bounds a variable's scope.
Well it's still technically a global state if it's a collection of scoped singletons, it's not much different than having a map of object names and their data as one big global variable, it's just formatted slightly more practically.
I'm the author. You don't exactly need the program to be small, you could do a lot with a small file (under 500 lines) being the only one able to access a global variable. People seem to think it's ok to leak data from your encapsulation when you're using a global which I tried to say is a bad idea in the article.
I've seen code for emulators that is very long functions and a lot of globals; I'd also argue they're OK if your program flow is very stable / predictable and synchronous, and especially older consoles and game engines have predictable phases. Getting information from your "tick" to your "render" stage is a lot easier if you have global state.
Emulators are often emulating "hardware globals", things like machine registers or GPU state where in the thing being emulated there really is only one instance.
That's a single mutable global with all application state, but inside that global are nested structs and the code working on those structs takes essentially a 'this' pointer. This approach works well since it enforces a strictly hierarchical code and data structure with a single root-struct (and also a single root-source-file) at the top.
A useless microbenchmark question: what is faster, accessing a global variable (which has to go through the GOT nowadays) or dereferencing a field in a structure that was passed to you by a pointer (a straightforward register+offset load)?
Hard disagree. Except for, begrudgingly, logging those are all bad uses and you shouldn't do them.
I find the append_work especially egregious. Never ever use a bloody global for that! Goodness gracious me. What an absolute and utter waste that provides zero utility. If you have a job system just pass a damn handle to the job queue. Good lord.
If everyone spent a little effort to not use globals the world would be a much better place. The value add of globals when they are mildly useful is so inconsequential such that it's basically never worth the effort.
I think the hatred of global variables comes from the fact that they are hard to use correctly. This article also makes the point that to use correctly you need to follow a bunch of rules.
I’m normally open minded about what I read posted here but that article does nothing but frustrate me. I can’t count the amount of times I’ve dealt with legacy spaghetti code that used global variables and the pain and suffering that results from that.
Exactly the issue; inevitably someone will forget to follow those rules, at which point esoteric bugs will have been introduced.
I think it’s somewhat similar to unchecked memory access. Used correctly it works just fine and offers extremely high performance. Unfortunately, history has shown that over enough time mistakes will be made. As an industry we’re now to the point of actively denouncing entire languages over the risks of unchecked memory access.
Using software development patterns that rely on the engineers to follow a bunch of rules to do things correctly will eventually burn you. Better to avoid them entirely if at all possible.
There's plenty of things in programming that are "hard to use correctly". I seen many codebases that you must clone an object because passing it in a function because there was so much spaghetti data structures that you'd end up accidentally mutating something you didn't mean
I commend author for not shying away from writing such a take on one of the generally assumed consensus about code.
However this just underlines again why globals are usually good to avoid. It takes rigor not to make mistakes that can completely mess up with program state.
I also strongly disagree with the benefits author advances for having global. Code feels actually less spaghetti when things are properly scoped and not accessible from everywhere, which hides extremely well dependencies and makes it way harder to reason about the code.
One famous exception to that is indeed logging (which in itself is based on global state when printing to stdout or stderr anyway).
And also
> on account of how few places your object can be stored to
you can store things in a single place, just once, without the need for globals.
> If you're using threads, global and static variables should be thread local. If they are not then the discussion becomes about sharing data across threads and synchronization, which is a different topic
Most languages make global and static variables thread-global by default, and making them thread-local is more work. I can see why, but that piece of language design causes a lot of global variable problems.
Also: you can simplify a lot of problems by deciding that something is going to be limited to n=1, whether that's variables or threads, and then a business reason comes along where you really want to have n=2. Suddenly every global is a source of regret.
There are many reasons why global variables are bad, but extremely high on the list (if not first) is concurrency issues. The fact that the author think it's acceptable in any C or C++ code base to put in code like:
static int prv_counter;
int counter() { return ++prv_counter; }
Is insanity. Like, this is programmer malpractice. This function can only ever be called from one thread (not to mention: using `int` instead of `int64_t` is also a trivial mistake, this easily overflows). This is the kind of thing that enters a code base, works fine for long enough that everyone forgets about it, then causes horrible security issues and crashes. The idea of saying this is "good use" of a global variable is... this person should not be giving advice on good coding.
If you want to do this (and you shouldn't, because global state is bad for 14 other reasons), at the very least, make it thread safe and not trivially overflowing:
Except the author's example is single threaded, so for that specific case your implementation of counter is needlessly complex and would actually be confusing.
The point is that there is no solution that works for all use cases. If you always attempt to write fully generalized code like that you'll end up with tons of unnecessary complexity. Solve the problem at hand, not some hypothetical.
The author even specifies a rule that covers your case:
"If you're using threads, global and static variables should be thread local. If they are not then the discussion becomes about sharing data across threads and synchronization, which is a different topic."
I'm sorry, but no. This is one of the primary reasons why global variables is bad, because of concurrency issues. If you add this to a code base, are you guaranteeing that at no point in the next decade, NOBODY is going to add a second thread? What if this is a library? What if some other library you're using runs a thread?
Doing this is planting a time-bomb in your code-base. It's going to lay dormant for a long time, and then explode, and be impossible to debug. This is why mutating global variables is `unsafe` in Rust, which is the right call. There is literally no C or C++ code base where I would allow this to pass code review, regardless of how single-threaded the author thinks it is.
> needlessly complex
It looks more complex than it is because I set the memory order to relaxed as an optimization. You could also write this:
If this was a junior making this mistake, I would sit down and talk to them and patiently and explain the issues. If you're writing a blog post on the internet saying "this is a good way to write code, do it like this!", I'm sorry, but I'm going to be very harsh. We haven't even discussed using `int` and not properly scoping the static, also rookie mistakes. Someone is going to read this article, and think this kind of code is ok. It is not.
> "If you're using threads, global and static variables should be thread local. If they are not then the discussion becomes about sharing data across threads and synchronization, which is a different topic."
This is nonsense. First off all, a counter like this wouldn't work if it was a thread_local, because then you'd get duplicate values. If duplicate values are ok, why is it global state at all? The only reason you'd make it global state is to produce unique values for that process run. And the general sentiment is like saying "this is how you drive drunk safely. We're not discussing keeping control of your car and crashing into a tree, that's a different topic".
This IS why global variables are bad! Not the only reason, but a pretty darn big one!
Not all the work we do is as part of large teams or on multimillion loc codebases.
In your context what you say is definitely the correct take, but I stand by my take that there are relatively simple programs written by individuals/small teams that don't need to be complicated by corporate scale concerns.
The problem is however that the internalized advice is shortened to "don't use global variables" and not "avoid global mutable variables when using concurrency".
Constant global variables can be very useful. Mutable global variables can be totally fine in a singlethreaded (e.g. typical embedded) program. I agree that you should still use them with caution, but every advice that says: "don't do $X" should come with instructions under which circumstances it is valid and under which it isn't — and how to get the intended behavior instead with e.g. message queues, locks or whatnot.
The point is obviously that the counter is centralized, and it relates to the previous example where is no concurrency. The need for synchronization when sharing data across threads is mentioned just below that.
It kinda does but perhaps not entirely. The increment will always end up in the right place afterwards but internally if you expect it to be +1 in the same thread you’ll sometimes be wrong
No, this is absolutely not true at all. Calling this function from multiple threads is undefined behaviour in C++ (unless synchronized using some other mechanism), you get NO guarantees what so ever on program behaviour.
Best case scenario is that the loads and stores are interleaved, which leads to multiple threads returning the same value when calling counter(), which will guarantee crashes elsewhere in the program (the purpose of functions like these is to produce UNIQUE values, after all). But it's undefined behaviour in any case, it's just unacceptable to put in a C/C++ code base.
I'm the author of the article. How many comments are you going to write about how my usage is bad citing multi-threads when I said multi threading is out of scope? And how do you not understand this would be a how-to use thread locals correctly when you are dealing with multiple threads.
I'm sorry you feel offended, but you did write an article online with a deliberately provocative title. If you do that, you need to be able to deal with criticism. And not considering concurrency when mutating globals in C/C++ is not acceptable (never mind good) practice. You can say "threads are out of scope" till your blue in the face, but if you write an article with the thesis "globals are good, actually", you have to be able to deal with people saying "they're dangerous because of thread safety". That is a legitimate criticism of your thesis. In addition, my other criticisms of your code (overflow and properly scoping statics) have nothing to do with concurrency.
Has anyone told you to never use atomics? Have you not heard lockless programming is hard? (I saw this recently https://wiki.libsdl.org/SDL2/CategoryAtomic,) Have you written multi-threaded programs? I written two large ones. The fact you're suggesting non-experts use atomic is insanity. As well as criticizing 'overflow' in an article showing minimum easy to understand code to be read by people using completely different languages
A toy mental model could be e.g. a function that takes OS/IO/GUI events and produces actions (which are then executed by the caller) and then iterate it as long as the program runs. This function in itself can be pure.
So did I, and then I did a stint in pico-8, stopped worrying and started loving the globals. It's my code, tiny, sloppy, single threaded, nobody reads it, it just needs to do a job.
Not going to use it in anything I get paid for though, lol. At one point there was a commonly accepted "singleton" pattern for things like logging but that's just globals with extra steps. In Go I just create services in my main and pass them around where needed.
I liked and used global variables, then I grew up and never used them, then I got more experienced and started to use them again in places where it makes sense ;) (same with goto btw)
Well sure, if you're a great programmer you can do all kinds of things that are potentially dangerous, like using globals or lots of goto for control flow.
We stick to these kinds of rules because most people are not great programmers all the time. It's just mostly better to do the safe and boring thing most of the time.
You can make globals thread safe by using thread locals. You can make methods using them reentrant by carefully saving and restoring state. What about exceptions? Any exception from `process()` is going to leave this global state in a total mess.
I should have talked more about that in the article. I mentioned defer, making functions reentrant, etc, but languages with exceptions and without defer can make things much harder. I tried to make it clear the global state should be accessible from a handful of functions or within a file/module
> A Few Rules For Using Globals:
> If you change observable state, restore it
I’m sorry but no. Humans are human and mistakes will be made. I’ve lost count of the number of esoteric bugs I’ve had to track down due to global state being changed and not put back properly.
If you have to qualify a pattern with rules that are easily forgotten or open to corner case bugs, it’s far better to just not use that pattern in the first place.
The author shows encapsulation of global state elsewhere. I’m not sure why they wouldn’t use RAII for the log level stuff so it was automatically rolled back.
I'm the author. I seen people make mistakes writing pure functions with many if statements in them. For a small period I heard people say loops should be banned. Would you want to go that far?
If your narrow the usage of a global within a file you can get a lot of mileage. That's not how people tend to use globals and why I wanted to write about it
Just two of the "rules" is enough to see that there's no sense here:
1. It should be hard or impossible to use incorrectly. For example, counter() keeps increments consistent.
2. If you change observable state, restore it.
That can be summarised as "To prevent mistakes: Don't make any mistakes". It made lots of sense once I saw this was by a C++ programmer, C++ is the language with, as a prominent C++ practitioner put it: False Positives for the question: Is this a valid program?
If you're used to a language which gaslights you by having the compiler not emit any diagnostics whatsoever and just calmly handing you a nonsensical output executable because what you wrote was subtly wrong obviously global variables seem fine, what's not to like? You just have to be inhumanly competent at all times, which was the baseline requirement for the entire language.
I had a feeling someone would bring this up (I'm the author.) Your state really shouldn't be depending on IDs or handles from a counter function. I'm not sure if most people can agree with what is considered using a global variable which is why I wanted to define it near the start
Yeah, I like how the OP basically recreates scoped variables by applying all these strict rules, instead of just using scoped variables. For example, with DI, instead of a global variable, we just use a singleton/scoped dependency. Why? Because we can enforce those rules implicitly without hoping everyone pinky promises to use the global variables correctly.
If you want to write code to show me what you're talking about (best if I can run it) I'll tell you why or why not. I can tell you right now I dislike DI (and singletons) for reasons I can't cover in a single post
Just use a flat C-style function API instead of a Singleton object. Singletons are only really needed in languages that enforce the 'everything is an object' folly.
Not exactly. For example, you can have a singleton object that maintains a persistent connection to a db to persist logs to. No one's going to inject the "ElasticsearchLogger" object in their method/class by accident, and even then, they'll only have access to the singleton state that the class lets them have access to. So now your private Counter variable is inside a global singleton without being accessible by anyone, even if that person is disregarding all of OP's rules.
A singleton object can encapsulate the global state, converting global variables to private fields. How would this be different? Because a counter singleton can for example disallow directly setting the count field, only allowing the count to be incremented through a method.
Yes good point. The module/unit acts as the singleton instance, in a sense, though that might be the incorrect way to put it.
In any case, I think variables that are “global” but encapsulated in this way lose the potential for harm we associate with a global variable the whole program may be directly reading and writing.
Your program must be really small and scoped for this to make sense.
Also, kudos for providing so many corner cases to avoid that are non-trivial to formulate.
It feels like one of those "it's not impossible to do right" cases.
I'll just cite one evaluation point from the post:
> It's extremely easy to use incorrectly.