Googles appar
Huvudmeny

Post a Comment On: cbloom rants

"11-23-12 - Global State Considered Harmful"

8 Comments -

1 – 8 of 8
Blogger Per Vognsen said...

As an API user, I usually want both the dynamically scoped option (your "global with inherits and overrides") and the explicit parameter passing option. Unfortunately, most APIs will give you one or the other, based on some misplaced sense of code aesthetics that values orthogonality over usability.

November 26, 2012 at 1:35 PM

Blogger Fabian 'ryg' Giesen said...

Options structs in C++ can be made to serve both of these needs reasonably well with chaining (setters that return *this). All code examples here: https://gist.github.com/4153316

First two examples should be obvious. Doing this like this also gives named parameters (which is one context where bools are OK).

For C APIs, there's a variant below the C++ code. The OodleWorkOpts* are just the same as OodleWorkOpts_Data*, but that's not exposed anywhere. So each of these functions just returns the pointer you pass in, but Defaults() and FromData() (FromData takes a struct that's supposed to be fully initialized) are the only functions in the API that actually give you an OodleWorkOpts* from something you can declare yourself.

That what, you can do the chaining equivalent, and you can also enforce that something is either explicitly initialized to Defaults or explicitly declared by the user to be fully initialized. (That said, I'd normally not even provide the _FromData variant unless it's *really* important).

Calling more than 2 or 3 such C-ish setter calls inside in a function call results in LISP-tastic nesting depth - the C++ version with references is slicker there. Then again, if you override more than 2 parameters, it's more readable to set up the options separately anyway, so that might be a good thing.

November 27, 2012 at 1:32 AM

Anonymous Anonymous said...

On the last point, what I've done in the past is made it so if you pass in NULL for (say) the Allocator, you get the default/global one; this keeps you from needing to expand the API excessively, and then internally the code just does "if (a==NULL) a = global;" at the top of each function.

One thing you have to be careful about in creating reusable code is balancing the API design between usability in big complex programs and in simple one-off programs. A big complex program can afford to write wrappers around your API to make it do what it wants--e.g. you could have no support for a global-ish API, leaving writing that to the app. For little programs, it should work immediately.

Thus, stb_image some global state (for HDR-to-LDR gamma conversion), which will suck for threads but isn't a common path and wasn't worth forcing extra complexity on simple apps. stb_malloc has no global state; if you want to use it for a global allocator you have to wrap it and define one explicitly and provide your own API for accessing it, because stb_malloc isn't intended for little one-off apps.

November 27, 2012 at 11:13 AM

Blogger Brian Balamut said...

Regarding #3, I've worked with a codebase that had that design - with a global 'manager' passed as an argument all over the gameplay code. This bloats things up pretty bad, all your function calls end up needing it if it really is one of those core global systems that's often used. What's worse, if you end up in some leaf function that needs it but doesn't have it, you have to refactor all access to it until you make it up the chain to some higher level that still has the manager argument - you end up changing the signature for multiple methods (and all usage) just to add one line of code.

The day I refactored this to just be a global object there was much rejoicing. It's not as pure, but the overall code quality is better. I still agree with your points, I just think this was a case where someone took the concept 'globals == bad' but didn't try to architect a viable alternative.

November 27, 2012 at 12:21 PM

Blogger Cyan said...

Yeah, i'm using the proposition n°3 almost all the time, although i tend to name it "void* context".
I like the idea that it is merely an (undescribed) void*, so that its content can change anytime later, without impacting existing user code.

In the end, it tends to look almost like an object. It's just that the internal variables are stored into a structure pointed at by the void* pointer, which must be explicitly passed along at each function call. So it adds one argument which could be avoided in a C++ object. But that's basically identical. and that's C :)

November 28, 2012 at 3:26 PM

Anonymous Anonymous said...

Hmm.. Aren't you essentially saying just make those chunks of code re-entrant?

December 9, 2012 at 11:41 AM

Blogger cbloom said...

"Hmm.. Aren't you essentially saying just make those chunks of code re-entrant?"

No, it's not exactly the same thing.

December 9, 2012 at 12:14 PM

Blogger cbloom said...

@Balamut -

yeah, I certainly am afraid of that.

I've worked in plenty of game codebases where you had to pass around a "World *" or an "Engine *" even though there only ever was one of them, and it just made the code generally more verbose and annoying than it needed to be.

So certainly it's not a no-brainer.

For low level helper libs (ala cblib or stb.h) my idea is that you would write the low level core code with no globals. eg. functions take Allocator * and Log * as arguments. But then you could/should also provide a helper that has global singleton instances of those objects, and gives you APIs that use the global version.

Or something.

December 9, 2012 at 12:18 PM

You can use some HTML tags, such as <b>, <i>, <a>

This blog does not allow anonymous comments.

Comment moderation has been enabled. All comments must be approved by the blog author.

You will be asked to sign in after submitting your comment.