Why are static variables bad?

Started by
56 comments, last by Sacaldur 10 years, 8 months ago

So what would be the correct approach in the example case, with the struct where a new member may be added? Where would the error be "reported" to, and in what way? More importantly, how would we know "whatever" was uninitialized, when its value as uninitialized may be undefined?

E.g.

FooContext fooContext;
fooContext.bla = <smth>; // old code that didn't know about the new "whatever" feature

// member whatever could have an undefined value, so how does this check that and "report" it, fail gracefully, etc.?
Foo *foo(new Foo(fooContext));

As like he says, it seems you need a constructor of some kind in fooContext to prevent this. Even if not one to force the programmer to pass a parameter, then at least a default one to initialize the variables so their values are all defined (perhaps so as to indicate special uninitialized states that Foo can then check for).

The ideal thing to do would be to treat the context as a volatile object and check for null on .bla before attempting to use it.

Do you mean a check on "whatever"? Since "whatever" was supposed to be the problem variable, as it was supposed to represent a "new feature" that had been added, and all preexisting code (i.e. before the feature addition) only knew of bla. The code already initializes bla. (Though I suppose maybe a check on bla would be good too, as someone could fail to init that, too) But where would this check go? If "in the code initializing the FooContext", then that means the programmer has to add it to each such piece of code, which creates a lot of duplicated checks, and more importantly, if you're doing that why aren't you just adding a proper initialization of whatever? And furthermore, the programmer adding such checks is then aware of the existence of the rest of the code, whereas kunos' scenario was that someone adds a new feature ("whatever" in this case) but isn't aware of/forgets to add the corresponding initialization to every place the FooContext is initialized. So if they are to add the checks to wherever the FooContext is initialized, then they know about those places, and so they should be adding proper initialization too, I'd think. But his scenario is that they are not so aware. It seems he wants it "robust against feature addition" or something like that. If the check goes in the constructor, then what about the concern about constructors being bloated with checks?

Advertisement

Do you mean a check on "whatever"? Since "whatever" was supposed to be the problem variable, as it was supposed to represent a "new feature" that had been added, and all preexisting code (i.e. before the feature addition) only knew of bla. The code already initializes bla.

I suppose I wasn't understanding the example very well. If "whatever" is a member added to the struct later then what would it being set to null matter for old code? Unless the "old code" tried to utilize it then it would essentially be the same as the transparent addition of adding a new variable to a class your code was using, but not utilizing that particular variable.

I'm assuming the point you're getting at is, someone adds the new "whatever" variable to an object, and your code attempts to use the "whatever" object, but your coworker managing the usage of the struct that passes it to you hasn't implemented setting that whatever to something yet. In that case of course you would have to check validity of the object before you use it. If I'm getting this wrong please do re-explain it to me.

But where would this check go? If "in the code initializing the FooContext", then that means the programmer has to add it to each such piece of code, which creates a lot of duplicated checks

It does, but in the case of a structure it's about the same as if you require a constructor to set the .bla and the .whatever and that whatever may have been changed to a null state after construction anyway.

What it comes down to is that you have two tools to an object really. A reference, which is a guarentee that the object is at least existant even if it may be in an invalid state for your usage, or a pointer, which can be null. I think as I read more into what you're saying, what you're getting at is that by using a constructor you are forcing the caller to pass a constructed object to your structure at the time of creation of the structure, and sure, that's true. But if the structure is designed to allow that information to change you'd have to use a pointer anyway, which would indicate you have to put the "redundant" error checking anyway. So again, it's a tradeoff really. You can use the construction power of the reference but you're only really avoiding a nullptr check, and you're saying explicit to the caller "you can't change this after creation."

And furthermore, the programmer adding such checks is then aware of the existence of the rest of the code, whereas kunos' scenario was that someone adds a new feature ("whatever" in this case) but isn't aware of/forgets to add the corresponding initialization to every place the FooContext is initialized. So if they are to add the checks to wherever the FooContext is initialized, then they know about those places, and so they should be adding proper initialization too, I'd think. But his scenario is that they are not so aware. It seems he wants it "robust against feature addition" or something like that. If the check goes in the constructor, then what about the concern about constructors being bloated with checks?

I'd agree in the case of adding new required dependencies to an object the system of requiring a reference to be passed is more safe, I can't debate that. But like I said you're not actually arguing against having a default constructor in that case you're simply arguing that it is more clear that an object with a constructor requiring a reference is more "informative" and "complicance requiring" than an object that can be changed after construction. Even if you only had one constructor, if you gave a method to change that member at all then you would inherently have to do the same nullptr check as providing a default constructor would.

Thus at the heart of it, the only real debate here is one of style: do you prefer each object to be different and ask for what it deems to be "required" dependencies at construction, or do you make setting the information more transparent and allow a default constructor as well as the overloaded ones of the same style. It's arguably not a very big difference but it has a big effect on how the code is portrayed to others.

Like the SFML example I was using, it isn't as big a deal as you're making it out to be, in the example of the sprite object it really isn't that unintuitive to "know" that you have to set a texture to the sprite before using it, even if you construct it without passing one. However since quite a few people seem more interested in removing "dangerous" code like a default constructor I may experiment more with different mixtures of both methods in some future projects to see how it works out in real world example. My biggest problem with debates using simple class like a foo or a baz or a whatever is that they don't take things like multiple dependencies and subsystems into account or the mutable nature of a lot of game objects. Most game objects are going to have a lot of state you can set after creation, perhaps even major systems, so in that the "reference only" constructor quickly becomes an impossibility.

Clearly a constructor will not fill a dynamic job like that, even if you may use one to set an initial state.

Its kind of like:


Car myCar;
myCar.Drive(); // error message: can't drive without an engine!
Engine engine;
myCar.SetEngine(engine);
myCar.Drive(); // error message: engine is missing spark plugs
SparkPlug plug;
engine.SetPlug(plug);
myCar.Drive(); // error message: car can't drive without a fuel tank
Wouldn't that drive anybode crazy? If a car can't drive without an engine, it shouldn't be constructable without an engine. You might want to have a setter method to change the engine later, but whats the point of building/constructing it so that you can't even use it without adding all other parts?

Yes, and in your example you're talking about one dependency. I don't see how it is much more clear to have a constructor that is default and one that asks for an Engine, and then to have one setter for an engine as well. Literally the only difference you're making is that you're forcing an assumption that some kind of engine must be passed into the object at creation.

I've been working in games for decades, and I've never seen a 'real' game try to do what was done in the snippet above.

Most games have many phases of construction for a game object.

There is the game object's basic default constructor. This should generally do nothing. In cases such as serialization you are going to overwrite all the data inside the object anyway, so work in the constructor will be thrown away. That doesn't mean the object isn't fully created; think of it more like you could consider a file stream that hasn't been opened or otherwise attached to any resources.

There is a virtual function on game objects for when they are created. This might be used by world builders or when objects are created at run time. Do the initialization here. At this point it still should not be hooked up to any resources.

There is a virtual function on games objects for when they are started. This function is called after deserialization or creation. This might be after the level has loaded but before the game goes live to the player. This will also happen when objects are created at run time. Resources get requested here. Also hook up interactions and other components here.

There is a virtual function on game objects for when they are in the world. This lets the object hook itself up with other objects in the room or proximity. They might also set special flags such as requests to not be culled, or to set a non-standard culling distance. Some objects and triggers may not need to be placed in the world to be functional.

Many large games also have spatial culling. If so, there will likely be virtual functions when the object enters the simulated area and when they leave the simulated areas.


If all of those are true, there are several stages of initialization: constructed --> created --> started --> in world --> visible. It is possible to go back through the hierarchy as well, remove 'visible' when they are culled, remove 'in world' when they are removed from world, remove 'started' when the object is serialized or stopped for other reasons.

Not that any of this has much to do with shared state using static variables.

I've been working in games for decades, and I've never seen a 'real' game try to do what was done in the snippet above.

Most games have many phases of construction for a game object.

There is the game object's basic default constructor. This should generally do nothing. In cases such as serialization you are going to overwrite all the data inside the object anyway, so work in the constructor will be thrown away. That doesn't mean the object isn't fully created; think of it more like you could consider a file stream that hasn't been opened or otherwise attached to any resources.

There is a virtual function on game objects for when they are created. This might be used by world builders or when objects are created at run time. Do the initialization here. At this point it still should not be hooked up to any resources.

There is a virtual function on games objects for when they are started. This function is called after deserialization or creation. This might be after the level has loaded but before the game goes live to the player. This will also happen when objects are created at run time. Resources get requested here. Also hook up interactions and other components here.

There is a virtual function on game objects for when they are in the world. This lets the object hook itself up with other objects in the room or proximity. They might also set special flags such as requests to not be culled, or to set a non-standard culling distance. Some objects and triggers may not need to be placed in the world to be functional.

Many large games also have spatial culling. If so, there will likely be virtual functions when the object enters the simulated area and when they leave the simulated areas.


If all of those are true, there are several stages of initialization: constructed --> created --> started --> in world --> visible. It is possible to go back through the hierarchy as well, remove 'visible' when they are culled, remove 'in world' when they are removed from world, remove 'started' when the object is serialized or stopped for other reasons.

Not that any of this has much to do with shared state using static variables.

And here I was starting to wonder if I was the only one that had objects that exist in different states and have information changed both during runtime and in different stages of setup and serialization. Guess I'm not totally crazy.

But yes, we have gone off on a bit of a tangent, though it's a good topic to bring to the attention of people because they're most assuredly going to run into it at some point in game development.

Do you mean a check on "whatever"? Since "whatever" was supposed to be the problem variable, as it was supposed to represent a "new feature" that had been added, and all preexisting code (i.e. before the feature addition) only knew of bla. The code already initializes bla.

I suppose I wasn't understanding the example very well. If "whatever" is a member added to the struct later then what would it being set to null matter for old code? Unless the "old code" tried to utilize it then it would essentially be the same as the transparent addition of adding a new variable to a class your code was using, but not utilizing that particular variable.

I'm assuming the point you're getting at is, someone adds the new "whatever" variable to an object, and your code attempts to use the "whatever" object, but your coworker managing the usage of the struct that passes it to you hasn't implemented setting that whatever to something yet. In that case of course you would have to check validity of the object before you use it. If I'm getting this wrong please do re-explain it to me.

The example appears to be: We have a class "Foo" which takes a "FooContext" to specify how to set it up. Initially, this "FooContext" contains only one parameter, "bla". Bits of code are created that have to make Foos from a FooContext, and naturally, they only set bla. But then down the road as our project progresses, someone decides now to add a new feature to Foo, which now requires a new parameter, "whatever", in the FooContext. And in how I imagine this scenario, which may or may not be how kunos imagined it, I imagine "whatever" to be a strictly "extensional" feature, i.e. it does not alter the original behavior of Foo, but extends it to add capability, that is, that the old way of using Foo should still work and not break. (The reason for this requirement is because without it, we might be forced to change the code anyway, but I'm trying to isolate the addition of the data field itself as the problem) But the programmer adding the new "whatever" parameter forgets a place where FooContexts are used and so doesn't add the proper initialization to the extensional feature whatever. The constructor for Foo now gets gibberish in the whatever field and crashes or behaves in some unpredictable ("undefined") way. It's not that the old code (here, meaning the code that makes the FooContext and the Foo) attempts to use "whatever", it's that the old code doesn't use "whatever", and because it was not updated to take "whatever" into account (meaning, it doesn't even set it to null, it doesn't do anything with it, period), causes Foo's constructor to gag.

So do you add the check for "whatever" not being set in to the old code, in which case it would probably be easier simply to just properly initialize whatever, or do you add the check in Foo, and regardless of the check placement, do you need to add a constructor to FooContext that ensures a stable "uninitialized" or "null" value that can be checked for? Because if you have to add the check and/or set-to-null in the old Foo-using code, then if you forget to add it there, you have a crash or other failure. To me, it seems the only way to make it robust against such omission is to put a constructor in FooContext that pre-initializes whatever to null.

The constructor for Foo now gets gibberish in the whatever field and crashes or behaves in some unpredictable ("undefined") way. It's not that the old code (here, meaning the code that makes the FooContext and the Foo) attempts to use "whatever", it's that the old code doesn't use "whatever", and because it was not updated to take "whatever" into account (meaning, it doesn't even set it to null, it doesn't do anything with it, period), causes Foo's constructor to gag.

So do you add the check for "whatever" not being set in to the old code, in which case it would probably be easier simply to just properly initialize whatever, or do you add the check in Foo, and regardless of the check placement, do you need to add a constructor to FooContext that ensures a stable "uninitialized" or "null" value that can be checked for? Because if you have to add the check and/or set-to-null in the old Foo-using code, then if you forget to add it there, you have a crash or other failure. To me, it seems the only way to make it robust against such omission is to put a constructor in FooContext that pre-initializes whatever to null.

Why would it crash on "whatever" if none of the old code uses it? You literally could have an object with two members, one being a pointer set to gibberish and one being a pointer set to a valid object and as long as you never tried to -use- the gibberish pointer it wouldn't actually do anything undefined.

The constructor for Foo now gets gibberish in the whatever field and crashes or behaves in some unpredictable ("undefined") way. It's not that the old code (here, meaning the code that makes the FooContext and the Foo) attempts to use "whatever", it's that the old code doesn't use "whatever", and because it was not updated to take "whatever" into account (meaning, it doesn't even set it to null, it doesn't do anything with it, period), causes Foo's constructor to gag.

So do you add the check for "whatever" not being set in to the old code, in which case it would probably be easier simply to just properly initialize whatever, or do you add the check in Foo, and regardless of the check placement, do you need to add a constructor to FooContext that ensures a stable "uninitialized" or "null" value that can be checked for? Because if you have to add the check and/or set-to-null in the old Foo-using code, then if you forget to add it there, you have a crash or other failure. To me, it seems the only way to make it robust against such omission is to put a constructor in FooContext that pre-initializes whatever to null.

Why would it crash on "whatever" if none of the old code uses it? You literally could have an object with two members, one being a pointer set to gibberish and one being a pointer set to a valid object and as long as you never tried to -use- the gibberish pointer it wouldn't actually do anything undefined.

The way I interpreted the example, the crash occurs when Foo's constructor is invoked with the FooContext having an uninitialized whatever member, and the crash occurs when Foo's constructor is invoked and chokes.

The way I interpreted the example, the crash occurs when Foo's constructor is invoked with the FooContext having an uninitialized whatever member, and the crash occurs when Foo's constructor is invoked and chokes.

Well then that means whoever wrote Foo is expecting to use whatever and shouldn't assume that it isn't null if it is a pointer. If you check that it's null and return or log the error or throw it then the person calling knows they never passed anything in for it. Saying that you -have- to use a constructor with a reference just to make it throw an error is a little asinine, it also severely limits the behavior of your object just because you're worried about someone not getting a compiler error because they flat out did not pass an object in.

Using frob's example it would be like calling a filestream a badly designed object because you allow people to create it without providing a file to open, and thus if they use it and it reports an error state then it would have been fixed by demanding they pass a reference. Obviously both popular libraries and even standard library objects do not adhere to always requiring a reference to be passed in for dependencies on each object.

The obvious way in C++ is to have:

class Ship

{

public:

void rotate(float angleRad); // THIS WILL ROTATE BOTH ANGLE AND UPDATE THE DIRECTION VECTOR SO THEY ARE ALWAYS ALIGNED

float getAngle() { return angle;}

Vector2 getDirection() {return direction;}

private:

Vector2 position;

Vector2 direction;

float angle;

};

The calculation is only done once, every time "rotate" is called, Ship is always valid. Welcome to 2013.. enjoy your 1960 language ;)

IMO, "angle" is not needed at all.

It seem to me that it not resolve the root core question i am talking about. even if one will have some clear syntax

that setting angle = 10 will update the values of angle.nx

angle.ny to read - it is still unpleasant jump between setting

angle.nx angle.ny and using it. Such jump may be less or more problematic in practise I think but it seem to be real troublemaking.

Yet another somewhat related example (it is not easy to give

examples here for me because it is somewhat foggy topic for me):

I got a global shared Sleep value (in miliseconds) which is used in the winapi dispatch messages loop

It is used yet in my some keyboard handlers module so I

could +- it from keayboard (to incrase/decrease fps and corresponding cpu usage)

I can not clearly decide where this variable should belong

to my keyboard handler module or to dispatch messages loop (?) I placed it yet in the third place in globals as far as I remember.

This 3 places is already a mess for me - it become yet worse becouse I had a system of including and excluding some game modules to my system and in such module sometimes I like to overvrite such Sleep value for debug purposes locally, set it 5 in one module to 15 in other I am working on (other way I would must find a initialisation and change it there so it it is yet worse)

As described above such global shared Sleep is an example of such global shared variable that brings a trouble (As I said before not all globals make such troble, some of them never do for me really and those troubles are more related to way of treating some globals not to all of them )

So how would someone resolve such Sleep trouble ?

So how would someone resolve such Sleep trouble ?

with proper software design.

The very fact that you consider this an issue shows that there is no proper design into your code.. you just make everything visible to everything else and call it a day... give it some time and you'll see this will create a spaghetti mess impossible to maintain.

If a variable has to be visible to 3 subsystem it doesnt mean it has to be visible to the entire software.

From my point of view, the situation is straightforward: the var belongs to the subsystem using it (in your case, whoever is doing the game loop) and is "exposed" to a user interaction layer (in your case, a keyboard "manager").

There are real cases for "globals".. as somebody was already pointing out, user "vars" might be a good candidate for it. At the end it's all about making decisions and live with those. For example, in my current game I have a quake like "console".. that is exposed through loose functions instead of a class.. so it's a kind of a singleton. User "vars" are published to the console for user interaction accessing this global state through these functions. It is a convenient decision because it makes this particular process less tedious but it comes at the known price of coupling every class using the global console to it without a CLEAR dependency relationship expressed via constructor.

@Frob.. I think the misunderstanding here is due to the fact that we are probably looking at different parts of a game.

For GameObjects serializable classes what you say makes perfectly sense.. it's mostly data driven, designed to be built and sculptured at runtime. But "engine" classes that handles hardware resources do require a much clearer initialization and dependency rules.. you can't build this if you don't have that... DX11 is a perfect example of this.. you cannot have a View to a resource if you dont have a Resource! It wouldn't make any sense.

GameObjects are an exception to that because their point is to be able to express dynamic relationships, uncertain content and interactive manipulation (ie from game editors).. you need to build an empty GameObject because that's what you'll be doing while editing the game. but would you build a MeshRenderer component without a reference to some sort of render manager thingy? And then have your code checking "if (renderManager)" everywhere? If the answer is yes then sorry.. I don't care how many games you have worked with that approach.. but it doesnt make sense AT ALL.

Stefano Casillo
TWITTER: [twitter]KunosStefano[/twitter]
AssettoCorsa - netKar PRO - Kunos Simulazioni

This topic is closed to new replies.

Advertisement