11

I was reading the "C++ Coding Standards" and this line was there:

Variables introduce state, and you should have to deal with as little state as possible, with lifetimes as short as possible.

Doesn't anything that mutates eventually manipulate state? What does you should have to deal with as little state as possible mean?

In an impure language such as C++, isn't state management really what you are doing? And what are other ways to deal with as little state as possible other than limiting variable lifetime?

8 Answers 8

16

Doesn't any mutable thing really manipulate state?

Yes.

And what does the "you should have to deal with little state" mean?

It means that less state is better than more state. More state tends to introduce more complexity.

In an impure language like C++, isn't state management really what you are doing?

Yes.

What are other ways to "deal with little state" other than limiting variable lifetime?

Minimize the number of variables. Isolate code that manipulates some state into a self-contained unit so that other code sections can ignore it.

9

Doesn't any mutable thing really manipulate state?

Yes. In C++, the only mutable things are (non-const) variables.

And what does the "you should have to deal with little state" mean?

The less state a program has, the easier it is to understand what it does. So you shouldn't introduce state that isn't needed, and you shouldn't keep it once you no longer need it.

In an impure language like C++, isn't state management really what you are doing?

In a multi-paradigm language like C++, there's often a choice between a "pure" functional or a state-driven approach, or some kind of hybrid. Historically, language support for functional programming has been fairly weak compared to some languages, but it's improving.

What are other ways to "deal with little state" other than limiting variable lifetime?

Restrict the scope as well as the lifetime, to reduce coupling between objects; favour local rather than global variables, and private rather than public object members.

5

state means that something is being stored somewhere so you can refer to it later.

Creating a variable, creates some space for you to store some data. This data is the state of your program.

You use it to do things with, alter it, compute with it, etc.

This is state, whereas the things you do aren't state.

In a functional language, you mostly deal only with functions and passing functions around like they were objects. Though these functions don't have state, and passing the function around, introduces no state (besides maybe inside the function itself).

In C++ you can create function objects, which are struct or class types which have operator()() overloaded. These function objects can have local state, though this is not necessarily shared among other code in your program. Functors (ie function objects) are very easy to pass around. This is about as close as you can imitate a functional paradigm in C++. (AFAIK)

Having little or no state means you can easily optimize your program for parallel execution, because there's nothing that can be shared among threads or CPU's, so nothing that contention can be created about, and nothing you have to protect against data races, etc.

2

Others have provided good answers to the first 3 questions.

And what are other ways to "deal with as little state as possible" other than limiting variable lifetime?

The key answer to question #1 is yes, anything that mutates eventually impacts state. The key then is to not mutate things. Immutable types, using a functional programming style where the result of one function is passed directly to the other and not stored, passing messages or events directly rather than storing state, calculating values rather than storing and updating them...

Otherwise you're left with limiting the impact of state; either via visibility or lifetime.

1

And what does the "you should have to deal with little state" mean?

This means that your classes should be as small as possible, optimally representing a single abstraction. If you put 10 variables in your class, most likely you are doing something wrong, and should see how to refactor your class.

1

To understand how a program works, you must understand its changes of state. The less state you have and the more local it is to the code that uses it, the easier this will be.

If you've ever worked with a program that had a large number of global variables you would understand implicitly.

1

State is simply stored data. Every variable is really some sort of state, but we usually use "state" to refer to data that is persistent between operations. As a simple, pointless example, you may have a class that internally stores an int and has increment() and decrement() member functions. Here, the internal value is state because it persists for the life of the instance of this class. In other words, the value is the state of the object.

Ideally, the state that a class defines should be as small as possible with minimal redundancy. This helps your class meet the single responsibility principle, improves encapsulation and reduces complexity. The state of an object should be entirely encapsulated by the interface to that object. This means that the result of any operation on that object will be predictable given the semantics of the object. You can further improve encapsulation by minimizing the number of functions that have access to the state.

This is one of the major reasons for avoiding global state. Global state may introduce a dependency for an object without the interface expressing it, making this state hidden away from the user of the object. Invoking an operation on an object with a global dependency may have varying and unpredictable results.

1

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 mean?

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).

enter image description here

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.

enter image description here

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.

State 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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.