At a glance 👀
While writing some code the other day, a colleague noticed that some code that was meant to only run under a specific but common condition was actually never getting called. It took us a while to track down the issue, not because it was particularly hard to solve — but because it was a single character typo that was incredibly hard to see!
No specific knowledge of UE4 or game engines is required to follow this article, though it may help; this is possible in any C++ code.
- C enums can be implicitly converted to int
- Pointers can be added and subtracted to and from
- Use enum class instead of enum
In UE4, an Actor is a class that interacts with the “real world” in the game. For example, this might be a car, a horse, or a power-up. Actors can “own” other actors. In our case, a Player may own a Sword. They’re two distinct actors with their own behaviours, but in this case the sword clearly belongs to, or is owned by, the player.
Each actor has a net mode. You don’t need to know what a net-mode is for the purposes of this article, just know that it could be one of the following values: NM_Client
, NM_Standalone
, NM_DedicatedServer
.
Consider the class AActor
, which exposes at least the following two functions:
There are cases where the owner of some actor has a different net mode to the actor itself. This might be if say, we’ve attached a purely client-side visual effect to a character, like a lit lamp.
In this case, from the lamp’s code, we may want to know what the net mode of our owning actor is. This is a very simple piece of code to write:
However, it’s possible to make a hard-to-spot typo that completely changes the meaning of the code.
See the typo?
Here, instead of asking our owner what its net mode is, we’re in fact taking the address of our owner, and subtracting our current net mode from it.
“Surely!” I hear you say, “this won’t compile. You’re mixing types with subtraction!”
The effect of having this typo is that the comparison would always return false, meaning expected behaviour wasn’t being run.
Let’s take a look at the definition of ENetMode
.
It’s a regular C style enum
. It is not a C++ enum class
, and this is important. (These are known as unscoped enums and scoped enums respectively. You can also use enum struct
instead of enum class
.)
GetOwner()
returns a pointer. It’s perfectly valid to add and subtract numbers to and from pointers; in fact this is a regular idiom in C code and low level C++ code. Pointer arithmetic lets you iterate through blocks of memory (for implementing arrays), and store data at specific memory locations.
Note that adding 1 to a pointer won’t increase the memory address by 1, it will add it by 1 * sizeof(T)
, where T
is the type you’re pointing to. We’ll see an example of this in a moment.
Here’s the fun bit: When you add an enum
to a pointer, C++ will cast the enum
to an int
(or a larger integral type if required to hold the value, such as long long
), and then add that to the pointer.
Let’s pull this together:
In the same vein as above — first the compiler converts the result of GetNetMode()
to an int
and subtracts it from the result of GetOwner()
, creating a new pointer. This pointer is then compared to NM_DedicatedServer
, both sides are converted to a matching integral type, and then they’re compared.
This is clearly confusing behaviour, and it’s easy to make this fail at compile time.
Instead of using enum
, we should declare ENetMode
using enum class
or as so:
There are a multitude of benefits to using enum class
, but the one we care about is that implicit type conversion is no longer allowed. (Using static_cast
to explicitly convert is all good though.)
Implicit type conversion can cause all types of unexpected headaches, so opting for writing code that requires us to be more explicit in our intent can save a lot of effort down the line.
enum class
is one example of how we can write code that helps people using our code use it more correctly.