Avoiding Global Variables

Started by
70 comments, last by Telastyn 12 years ago
You think improved OO design would have helped achieve this?[/quote]

Less OO would help.

No longer code related, but same principles apply to management.

Basic singleton design:
- Joe is in charge of procurement. It works great at a start-up with 5 employees. "Joe, I need a new 1TB disk". Start-up grows. "joe, we need 20 new blades for the rack". Grows further. "Joe, we need to ressuply our 16,000 vendor partners in EMEA in full compliance with the law in each of 43 countries".

Theoretical company works like that. Boss -> Subordinate -> Subordinate -> .... In theory it works. In practice it no longer does. While formal chain of command remains, just about all companies transformed into a flatter structure. Individual units have more control and independence.

So it becomes this:
First there's Joe. Then there's 5 people in charge of procurement. Then there's 20 people. Then there's 200 departments with 50 people each.

Distinction is crucial. In first case, procurement is about Joe. "Joe, procure ....". As company grows, it has no process to delegate this to someone else.

In latter case, procuremnt is about obtaining stuff, with different hierarchies and structures involved. Process is set up for growth since start.
---

If thinking about some basic trivial project, not thinking about that is perfectly fine. Just like example above will likely grow to several hundred people, so will code last for a while.

At same time, all current studies of corporate development strongly emphasize distributed control and individual involvement rather than top-down org-chart.

Reason is simple. When it comes to org charts, the Big Name Orgs (think 100k employees or more) have it down. If someone needs to get 10,000 people going, they'll do it before breakfast.

So to compete, you need a different strategy, one that competition isn't capable of. And that is breadth.

Management examples aren't made up, they're simple fact of survival. Companies that rely on top-down management either died, exist due to government protection or will die very soon. World no longer tolerates single points of failure.
Advertisement

[quote name='Washu' timestamp='1332497186' post='4924570']
Yeah, "chances are" you'll never have more than one Sockets system on your platform which needs to be setup and cleaned up. But I'd say it goes a fair bit beyond "chances are".


Really? I'm pretty positive that in most games that use sockets you're more likely to encounter multiple sockets being required by the application. [/quote]

Multiple sockets - yes. Multiple Sockets systems - no.

On Win32 platforms, the "WSAStartup" function initiates use of the Winsock DLL by a process. It must be called in order for the program to use any Sockets functionality. But no matter how many "sockets" the program uses, the DLL should only be initialized once (per cleanup). Off the top of my head, there are three general options for managing this sort of thing:

  • Dump setup and cleanup code into a couple of big setup and cleanup functions for the application.
  • Pass around a factory object whose sole purpose is to own networking objects' lifetimes, so it can do setup and cleanup as needed.
  • Statically "reference count" all networking objects to do setup and cleanup as needed.


I'm not married to it, but I like the last option because it presents the bare-bones simplest interface to the world. Just create, use, and destroy networking objects whenever and however you want.
[/quote]
On Win32, You can safely call WSAStartup as many times as you want without harm. This is explicitly noted in the documentation. Furthermore, you need to call WSACleanup once per WSAStartup call. Hint: This sounds like a prime place for a constructor and destructor.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

On 3/23/2012 at 11:33 AM, Washu said:

On Win32, You can safely call WSAStartup as many times as you want without harm. This is explicitly noted in the documentation. Furthermore, you need to call WSACleanup once per WSAStartup call.

Yeah, it says,

Quote

...if an application calls WSAStartup three times, it must call WSACleanup three times. The first two calls to WSACleanup do nothing except decrement an internal counter; the final WSACleanup call for the task does all necessary resource deallocation for the task.

Although it should be noted that the reason the documentation mentions for calling it multiple times is to attempt initialization on different WinSock revisions. Making spurious calls throughout execution may or may not be something you want to do. The only thing the documentation is promising is that it's legal. But this is telling us that Win32 is, itself, using this sort of static "reference count" technique for us. And the result is zero complication added to the API for this issue (including no static init nightmeres).

Anyway, the point was simply that the notion, of there being things here and there that are quite singular in nature, is not far-fetched (the occasional implementation loophole provided by a Win32 API notwithstanding).

It simply does not matter whether there are true singularities in nature—the only place globals have in coding is where they are the only possible way to achieve something.

I want all of my textures to have a unique ID. A static ID counter and critical section is the only way to achieve this.
If anything is possible without globals, it is wrong to use globals.

You would think that your game can have only one window, and only one game instance. Until you realize that having multiple windows could be helpful for debugging.
You will think it is fine to have only one OpenGL context and keep it as a global. Until you start making tools in Qt and realize that each OpenGL view has its own context and your resources won’t work between them.

I have made a few things globals long ago in my inexperienced days and have been kicking myself in the ass ever since. They sure seemed appropriate at the time. “Gosh, when would I ever need more than one of these?”, I thought. “Oh. When I start taking my project seriously,” I replied.

Not to mention the security risks. A global game class?? Talk about easy hacking. Just find the static base pointer and you are on your way to the characters and enemies, followed by auto-aim, bots, what-have-you. And be sure to use MHS for all your hacking needs.

I have only ever used one menu manager per project ever. But there is no justification for making it a global. It doesn’t make sense. As an instance, I know exactly what can access it, when, and how. I know when it will be freeing its memory and leaving existence and when it will be coming back as a new instance.
Which brings up another point. How often do you destroy your globals? Do you not feel the need to release some of that memory that doesn’t need to be used?

How do you feel about going into a part of the game in which there is no menu, but having residual memory-manager residue laying around in the background?
Do you plan to make a global system to tell each global system when to release and renew its resources? Good luck managing that mess.


Globals: They just don’t make sense.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid


I want all of my textures to have a unique ID. A static ID counter and critical section is the only way to achieve this.

No globals needed there either... pass around the texture cache to the appropriate things that should be loading textures (hint, there aren't that many things that SHOULD ever load a texture).
class TextureCache {
private:
int currentTextureId;
std::mutex cacheLock;
//...
public:
int LoadTexture(std::string const& filename);
void ReleaseTexture(int textureId);
};

and for those who speak with a Porsche Owners Club accent and go "Ahh yes, but we're on C!"
typedef struct TextureCache_t {
int currentTextureId;
MutexHandle mutex;
//...
}TextureCache, *TextureCachePtr;

int LoadTexture(TextureCachePtr cache, char const* filename);
void ReleaseTexture(TextureCachePtr cache, int textureId);

Similarily, on the WinSock front... no global there either. A static instance, but that's for convenience only. Plus the class can be instantiated more than once without harm. Its also nicely hidden behind an anonymous namespace, and if placed properly within a singular compilation unit, will be entirely invisible to the rest of the code.
namespace {
struct SocketInit {
SocketInit() {
WSAStartup(MAKEWORD(2,2), &data);
}

~SocketInit() {
WSACleanup();
}
WSADATA data;
private:
static SocketInit socketInit;
};
SocketInit SocketInit::socketInit;
}

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Whoa, another thread turnig philosophical? Well, minimizing relationship is always good. Global variables means, everything is dependent on everything else,... potentially. Keeping dependencies local means invariants are easier to maintain. Theoretically at least.

No globals needed there either... pass around the texture cache to the appropriate things that should be loading textures (hint, there aren't that many things that SHOULD ever load a texture).
class TextureCache {
private:
int currentTextureId;
std::mutex cacheLock;
//...
public:
int LoadTexture(std::string const& filename);
void ReleaseTexture(int textureId);
};

That is not my use for texture ID’s. The ID is not to be used to identify textures, it is to be used for debugging and optimization only.
The part related to debugging is primarily why every texture needs a unique ID regardless of how many texture caches or managers there are.
Additionally, for optimizations, this ID actually needs to be unique between both the render target systems and the texture systems.
Of course you could have a manager/cache at a lower level where the difference between a texture and a render target is abstract and modify your example accordingly, but that doesn’t take care of the debugging issue, and passing around the lowest-level manager for that system is undesirable—I want users to think of them as separate systems with only a few things in common, so I would be passing a TextureManager and a RenderTargetManager around, but one would really just be a pointer cast of the other and fairly redundant.

Of course just passing around a graphics device would solve that problem, but design issues are not the point here. It is more about using a system-wide ID, unrelated to the number of graphics devices or contexts you have, to aid in certain subsystems, including general-purpose debugging.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid


The difference and similarity between the two was, in fact, the point in question.

My apologies. Your response started with "In other words…" so I read the whole paragraph from that viewpoint, and believed you were conflating the two options instead of offering an alternate view.

The arguments for putting up with singletons because "I only need 1 instance right now" seems like a decent appeal to YAGNI, which I'd normally agree with... but only if you're writing "throw away" code, where you don't care at all about quality, and only if it's actually quicker and easier than writing quality code (which it's not).


I would argue that fewer lines of code is quicker and easier to write, and globals use demonstrably fewer lines of code. I'm not saying to use that metric to decide what approach to take, but it usually is quicker and easier - I feel it devalues your argument to suggest otherwise.

[quote name='Potatoman' timestamp='1332426409' post='4924321']
I'm seeing assumptions from both sides, one that you will need more than one of these objects in future, and the other a simplifying assumption that chances are you won't.

What of the simplifying assumption that this class doesn't support multithreading? Or do I have to support mulitthreading for all classes because if I scale the system up I will likely need that one day?


If you look at both assumptions though:

Assume that your code likely won't need more than one instance.

You can make global. If it's global and you were right, you saved negligible time. If it's global and you were wrong, you now have a large amount of painful rework.
You can make it not global. If it's not global and you were right, you spent a little bit of time to make the code more explicit (read: easier to pick-up/work with). If it's not global and your assumption was wrong, you pass in the new/different instance.
[/quote]

I see largely the reverse, though certainly it depends on the situation. In my experience, if you were wrong, you just wasted a potentially nontrivial amount of time on developing and supporting infrastructure you will never need. If you were right, you saved neglibigle time overall because you can go in after the fact and update it. The key benefit for building in the support is allowing rapid design changes at late stages of the project, at the cost of spending more time up front. This might be useful for a project, or it might not be.

Can you give an example of a situation where the rework is more significant than simply changing the singleton to be to passed in, that is not the result of poor design in general (as in, it's a painful rework because of the general code structure, not the use of a singleton). What I mean is if we're talking about singleton vs passing in the argument, I see no difference between implementing it now or later. Why is it more painful later? At a coarse level, I imagine the 'pain' of implementing it later is proportional to the amount of time saved by making this simplifying assumption in the first place.


making that sort of stuff global prevents doing that effectively.

It doesn't prevent it at all, instead of passing the parameter in, you set the global to that value. It might not be as 'clean' or as intuitive to the reader as you'd like, but I think 'prevents' is a bit strong.


void TestMyContainer()
{
std::unique_ptr<SharedData> data(new SharedData());
MyCustomContainer c(data);
c.DoSomething()
}


versus


void TestMyContainer()
{
std::unique_ptr<SharedData> data(new SharedData());
MyCustomerContainer::setSharedInstance(data.get());

MyCustomContainer c;
c.DoSomething()

MyCustomerContainer::setSharedInstance(nullptr);
}


Now again with the disclaimers, I'm not recommending a container class that uses a global shared instance, that's not the point. I'm merely illustrating that it doesn't prevent you from unit testing. Please can we refrain from asking why you would actually have a container taking a shared instance in the manner demonstrated above.

This topic is closed to new replies.

Advertisement