Towards less side-effects
by Malte Skarupke
C++ has very little protections against side effects in programming. You’ve got const and that’s about it. And const doesn’t survive through pointers so there’s nothing to stop you from modifying other objects as you please. I like things that stop you from making mistakes. So I thought about how to introduce more things that would make it more difficult to have side-effects in your programming.
I’ve found that you can reduce the side effects in your code by being careful about which pieces of code get dt, and by making all globals const, so that you have to manually pass non-const refs around and make them conscious decisions.
I’ve started thinking about this because I’ve had this requirement for my graphics programmer: The drawing code must not change anything else. Meaning if I call draw() twice without calling any other code inbetween I will draw the same image twice. Meaning the particles haven’t moved, the character’s animation hasn’t advanced etc.
So how do you enforce this? For drawing this one thing was enough: draw() doesn’t get the dt. Without the dt it should be impossible to change anything. Sure, you could theoretically hardcode your graphics code for a dt of 1/60.0f but then you’re intentionally causing bugs and the problems are at least obvious.
Since then we have run into two problems with this: The first thing was in our debug drawing: We wanted to display text that is visible for a set amount of time. The first implementation was to just reduce the life time of the text every time that it’s drawn. Which wasn’t possible without dt. Yay, bug prevented. The second thing was doing the animation of a water surface in the drawing code. I don’t quite know how it works but it turned out that we just need to pass a value to the shader that counts up with time. I asked our graphics programmer why we couldn’t change that value during the update phase and then read it when drawing. Turns out we could.
What are the benefits of this? Well obviously no side effects means it’s easier to change things. But there are non-obvious advantages, too: now that I can be certain that our draw() method has no side effects, I can call it as often as I want. For example when debugging an inner loop. I was working on our IK system and just looking at the bones’ data directly is painful. So to debug this I just put draw() into the innermost loop of that code and the screen is re-drawn every time that a single bone changes. Sure, I could have also debugged this by advancing the IK once per frame but this was my first implementation and that would have been a bit of extra work. The point is: If you are certain that some parts of your code don’t change any state, you can work differently with them. I can envision many engines where drawing in an innermost loop just to debug something would be considered a horrible thing to do. Here it is perfectly fine.
I do recommend that you do this. I have told other people about this “take the dt out of your drawing, it will prevent you from making mistakes” and some of the responses I have received have shocked me. One person disagreed with me and said “well draw may not need dt, but some of the code that it calls does.” Meaning this programmer was perfectly aware that the function draw() in her engine does more than just drawing and also calls other code, and thought that that’s how it should be. So yeah, try making the change of removing dt, and see where your code breaks. You may discover that some bad code has crept in there.
I’ve made many such discoveries after a second recent change that was made to reduce side-effects: I made all our globals const. Meaning if you have a global getWorld() or similar, make the reference it returns a “const World &” instead of a “World &”.
Once you have const globals you will discover that terrible things have been happening in your game code. In fact I have discovered terrible things in my own code where I really should have known better.
Ideally you would just get rid of all globals. But that is not realistic. Sometimes you just need the current value of gravity in a code that controls AI behavior and then it’s easier to just have a global phy::getPhysics().getGravity(). However if you make all your globals const, you can stop accidentally changing values.
Now in our engine if you want a non-const reference to anything, you have to pass it to the object somehow. This is usually not very difficult; there are still enough ways to get non-const references. (for example if you’re inside a Component on an Entity you can call the getOwner() function on that Entity to get a pointer to the world) But now every time that you have a non-const ref it is a conscious decision on someone’s part. This just forces you to think about it for one more second. And it is now more obvious when code will modify an object, because the only way to get non-const access to anything is to get a reference to it as a function argument.
The only exception to all of this is scripting. Lua code can receive non-const references to globals. For that I do just use a const_cast before passing pointers to luabind. Which is a good use of const_cast.
The point that not passing dt and only having const globals have in common is, that they are about which access you give to certain pieces of code. When you write code, think a lot about which information it actually needs. For example I recently implemented an animation blend tree for our engine. The nodes in the blend tree do only get a const pointer to the tree and a const pointer to the Entity. Simply because I only want them to do animation code. Other classes are responsible for modifying the tree and other classes may modify the Entity based on animation code. But by being careful about how I pass objects around I can be certain that no animation code has any side effects.
I am still looking for more ways to tell me when I am making mistakes. I have found the techniques described in this post to be useful. You can still make all the same mistakes, but chances are that now you will run into at least one problem before you make them. And that is pretty good.