Doesn't anything that mutates eventually manipulate state?
Yes, but if it's behind a member function of a small class that is the sole entity in the entire system that can manipulate its private state, then that state has a very narrow scope.
What does you should have to deal with as little state as possible
From the variable's standpoint: as few lines of code should be able to access it as possible. Narrow the variable's scope to the minimum.
From the line of code's standpoint: as few variables should be accessible from that line of code as possible. Narrow the number of variables that the line of code can possibly access (it doesn't even matter that much whether it does access it, all that matters is whether it can).
Global variables are so bad because they have maximum scope. Even if they're accessed from 2 lines of code in a codebase, from the line of code's POV, a global variable is always accessible. From the variable's POV, a global variable with external linkage is accessible to every single line of code in the entire codebase (or every single line of code that includes the header anyway). In spite of only being actually accessed by 2 lines of code, if the global variable is visible to 400,000 lines of code, your immediate list of suspects when you find it was set to an invalid state will then have 400,000 entries (perhaps quickly reduced to 2 entries with tools, but nevertheless, the immediate list will have 400,000 suspects and that's not an encouraging starting point).
Chances are, likewise, that even if a global variable starts out only being modified by 2 lines of code in the entire codebase, the unfortunate tendency of codebases to evolve backwards will tend to have that number drastically increase, simply because it can increase as many developers, frantic to meet deadlines, see this global variable and realize they can take shortcuts through it.
In an impure language such as C++, isn't state management really what
you are doing?
Largely, yes, unless you're using C++ in a very exotic way which has you dealing with custom-made immutable data structures and pure functional programming throughout -- it's also often the source of most bugs when state management becomes complex, and complexity is often a function of the visibility/exposure of that state.
And what are other ways to deal with as little state as possible other
than limiting variable lifetime?
All of these are in the realm of limiting a variable's scope, but there are many ways to do this:
- Avoid raw global variables like the plague. Even some dumb global setter/getter function narrows the visibility of that variable drastically, and at least allows some way of maintaining invariants (ex: if the global variable should never be allowed to be a negative value, the setter can maintain that invariant). Of course, even a setter/getter design on top of what would otherwise be a global variable is pretty poor design, my point is just that it's still way better.
- Make your classes smaller when possible. A class with hundreds of member functions, 20 member variables, and 30,000 lines of code implementing it would have rather "global" private variables, since all those variables would be accessible to its member functions which consist of 30k lines of code. You might say the "state complexity" in that case, while discounting local variables in each member function, is
30,000*20=600,000. If there were 10 global variables accessible on top of that, then the state complexity might be like
30,000*(20+10)=900,000. A healthy "state complexity" (my personal kind of invented metric) should be in the thousands or below for classes, not tens of thousands, and definitely not hundreds of thousands. For free functions, say hundreds or below before we start to get serious headaches in maintenance.
- In the same vein as above, don't implement something as a member function or friend function that can otherwise be nonmember, nonfriend using only the class's public interface. Such functions cannot access the class's private variables, and thus reduce the potential for error by reducing the scope of those private variables.
- Avoid declaring variables long before they're actually needed in a function (i.e., avoid legacy C style which declares all variables at the top of a function even if they're only needed many lines below). If you do use this style anyway, at least strive for shorter functions.
Beyond Variables: Side Effects
A lot of these guidelines I listed above is tackling direct access to raw, mutable state (variables). Yet in a sufficiently complex codebase, just narrowing the scope of raw variables won't be enough to easily reason about correctness.
You could have, say, a central data structure, behind a totally SOLID, abstract interface, fully capable of perfectly maintaining invariants, and still end up running into a lot of grief due to the wide exposure of this central state. An example of central state which isn't necessarily globally accessible but merely widely-accessible is the central scene graph of a game engine or the central layer data structure of Photoshop.
In such cases, the idea of "state" goes beyond raw variables, and just to data structures and things of that sort. It likewise helps to reduce their scope (reduce the number of lines that can call functions that indirectly mutate them).
Note how I deliberately marked even the interface as red here,
since from the broad, zoomed-out architectural level, accessing that
interface is still mutating state, albeit indirectly. The class can
maintain invariants as a result of the interface, but that only goes
so far in terms of our ability to reason about correctness.
In this case, the central data structure is behind an abstract interface which may not even be globally-accessible. It might merely be injected and then indirectly mutated (through member functions) from a boatload of functions in your complex codebase.
In such a case, even if the data structure perfectly maintains its own invariants, weird things can happen at a broader level (ex: an audio player may maintain all kinds of invariants like that the volume level never goes outside the range of 0% to 100%, but that doesn't protect it from the user hitting the play button and having a random audio clip other than the one he most recently-loaded start playing as an event is triggered which causes the playlist to reshuffle in a valid way but still undesired, glitchy behavior from the broad user perspective).
The way to protect yourself in these complex scenarios is to "bottleneck" the places in the codebase that can call functions which ultimately cause external side effects even from this kind of broader view of the system that goes beyond raw state and beyond interfaces.
As odd as this looks, you can see that no "state" (shown in red, and this does not mean "raw variable", it just means an "object" and possibly even behind an abstract interface) is being accessed by numerous places. Functions each have access to a local state which is also accessible by a central updater, and the central state is only accessible to the central updater (making it no longer central but rather local in nature).
This is only for really complex codebases, like a game which spans 10 million lines of code, but it can help tremendously in reasoning about the correctness of your software, and finding that your changes yield predictable results, when you significantly limit/bottleneck the number of places that can mutate critical states that the entire architecture revolves around to function correctly.
Beyond raw variables is external side effects, and external side effects are a source of error even if they're confined to a handful of member functions. If a boatload of functions can directly call those handful of member functions, then there's a boatload of functions in the system that can indirectly cause external side effects, and that raises complexity. If there's only one place in the codebase that has access to those member functions, and that one path of execution is not triggered by sporadic events all over the place, but is instead executed in a very controlled, predictable fashion, then it reduces complexity.
Even the complexity of state is a rather important factor to take into account. A simple structure, widely-accessible behind an abstract interface, is not so hard to mess up.
A complex graph data structure which represents the core logical representation of a complex architecture is pretty easy to mess up, and in a way that doesn't even violate the graph's invariants. A graph is many times more complex than a simple structure, and so it becomes even more crucial in such a case to reduce the perceived complexity of the codebase to reduce the number of places that have access to such a graph structure to the absolute minimum, and where that kind of "central updater" strategy which inverts to a pull paradigm to avoid sporadic, direct pushes to the graph data structure from all over the place can really pay off.